python函数式编程之装饰器

Posted renpingsheng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python函数式编程之装饰器相关的知识,希望对你有一定的参考价值。

1.开放封闭原则

简单来说,就是对扩展开放,对修改封闭

在面向对象的编程方式中,经常会定义各种函数。

一个函数的使用分为定义阶段和使用阶段,一个函数定义完成以后,可能会在很多位置被调用

这意味着如果函数的定义阶段代码被修改,受到影响的地方就会有很多,此时很容易因为一个小地方的修改而影响整套系统的崩溃,

所以对于现代程序开发行业来说,一套系统一旦上线,系统的源代码就一定不能够再改动了。

然而一套系统上线以后,随着用户数量的不断增加,一定会为一套系统扩展添加新的功能。

此时,又不能修改原有系统的源代码,又要为原有系统开发增加新功能,这就是程序开发行业的开放封闭原则,这时就要用到装饰器了。

2.什么是装饰器??

装饰器,顾名思义,就是装饰,修饰别的对象的一种工具。

所以装饰器可以是任意可调用的对象,被装饰的对象也可以是任意可调用对象

3.装饰器的作用

在不修改被装饰对象的源代码以及调用方式的前提下为被装饰对象添加新功能

原则:

1.不修改被装饰对象的源代码
2.不修改被装饰对象的调用方式

目标:

为被装饰对象添加新功能

4.装饰器的定义和使用

来看下面的代码:

    import time
    import random
    
    def index():
        time.sleep(random.randrange(1,5))
        print("welcome to index page")
    
    index()

index函数的作用是程序在随机睡眠1到5秒之后,打印一句话

现在想为index函数添加一个新功能:统计index函数的运行时间,该怎么做呢??

修改index函数如下:

    import time
    import random
    
    def index():
        start_time=time.time()
        time.sleep(random.randrange(1,5))
        print("welcome to index page")
        end_time=time.time()
        print("cost time: %s" %(end_time - start_time))
    
    index()

运行程序,执行结果如下:

welcome to index page
cost time: 2.000999927520752    

可以看到,为index函数添加新功能确实实现了,但是却违反了开放封闭原则。

在符合开放封闭原则的前提下,如果想为index函数添加新功能,此时就要使用装饰器了

修改代码

    import time
    import random
    
    def index():
        time.sleep(random.randrange(1,5))
        print("welcome to index page")
    
    def timmer():
        def inner():
            start_time=time.time()
            index()
            end_time=time.time()
            print("run time: %s " %(end_time-start_time))
        return inner
    
    f=timmer()
    f()

运行程序,查看执行结果

welcome to index page
run time: 1.0 

从程序执行结果可以看出,index函数的运行时间已经被统计出来了

但是查看源码可以知道,index函数的源码确实没有被修改,但是index的调用方式被修改了

而且还有一个问题就是,timmer这个装饰器只能被用来装饰index这个函数,如果以后想统计别的函数的运行时间,又要重新定义别的装饰器,这样也太不灵活了。

修改上面的代码

    import time
    import random
    
    def timmer(func):
        def inner():
            start_time=time.time()
            func()
            end_time=time.time()
            print("run time: %s " %(end_time-start_time))
        return inner
        
    def index():
        time.sleep(random.randrange(1,5))
        print("welcome to index page")
    
    index=timmer(index)
    index()

运行程序,查看程序执行结果

welcome to index page
run time: 4.0 

可以看到,index函数的源代码没有被修改,index函数的调用方式也没有改变,但是依然为index函数添加了统计时间的功能,这里使用的就是装饰器了。

来分析下上面代码的执行流程:

    1.导入time和random模块,定义index函数和timmer函数
    2.把原始的index函数的内存地址作为参数传给timmer函数。
    3.timmer函数内部嵌套定义一个函数inner,然后返回inner函数的内存地址
    4.timmer函数执行完成,返回timmer函数的内部函数inner的内存地址,然后把inner的内存地址赋值给index变量
    5.index是inner函数的内存地址,index变量加括号运行,实际上就是在运行inner函数
    6.运行inner函数,定义程序开始时间。
    7.执行timmer函数的变量func,在第2步知道,func这个变量就是index的内存地址,所以这里实际上是执行被装饰过后的index函数
    8.index函数执行完成,定义程序的终止时间
    9.统计并打印整个程序的执行过程中所花费的时间

这就是装饰器装饰index函数的执行流程

5.装饰器的简化使用

现在我又有另外一个函数home,现在我也想统计home函数的运行时间,可以把代码修改如下

    import time
    import random
    
    def timmer(func):
        def inner():
            start_time=time.time()
            func()
            end_time=time.time()
            print("run time: %s " %(end_time-start_time))
        return inner
    
    def index():
        time.sleep(random.randrange(1,5))
        print("welcome to index page")
    
    def home():
        time.sleep(random.randrange(1,5))
        print("welcome to home page")
        
    index=timmer(index)
    index()
    
    home=timmer(home)
    home()

运行程序,执行结果如下

welcome to index page
run time: 3.0 
welcome to home page
run time: 4.0 

可以看到,每次调用统计程序运行时间的装饰器timmer,都要先把被调用的函数的函数名作为参数传给timmer装饰器

然后再把timmer装饰器的执行结果赋值给被调用的函数名本身,最后才能调用被装饰的函数,太麻烦了有没有??

其实python中的装饰器可以简化成下面的格式

    import time
    import random
    
    def timmer(func):
        def inner():
            start_time=time.time()
            func()
            end_time=time.time()
            print("run time: %s " %(end_time-start_time))
        return inner
    
    @timmer
    def index():
        time.sleep(random.randrange(1,5))
        print("welcome to index page")
    
    @timmer
    def home():
        time.sleep(random.randrange(1,5))
        print("welcome to home page")
    
    index()
    home()

程序执行结果

welcome to index page
run time: 2.0 
welcome to home page
run time: 4.0 

可以看出,使用@加装饰器名添加到被装饰对象的上方的方式也可以为一个函数添加装饰器中定义的功能

6.多个装饰器的定义与调用

在上面的例子里,定义并调用了一个统计程序运行时间的装饰器timmer,

如果现在想为index函数添加一个用户认证的功能,可以定义一个名为auth的装饰器

    import time
    import random
    
    def auth(func):
        def wrapper():
            while True:
                user=input("Input your username>>>:").strip()
                pwd=input("Input your password>>>:").strip()
                if user== "abcd" and pwd == "abcd1234":
                    print("login successful")
                    func()
                    break
                else:
                    print("login error")
        return wrapper
    
    @auth
    def index():
        time.sleep(random.randrange(1,5))
        print("welcome to index page")
    
    index()

运行程序

    Input your username>>>:abcd             # 先输入错误的用户名和密码
    Input your password>>>:1234
    login error                             # 提示用户输入错误,登录失败
    Input your username>>>:abcd             # 让用户再次输入用户名和密码
    Input your password>>>:abcd1234
    login successful                        # 登录成功
    welcome to index page                   # 执行index函数

从程序执行结果可以看出,用户登录密码验证的装饰器auth已经定义并被成功调用了

如果想为index函数添加用户认证的功能,又想统计index函数执行时间的功能,在使用装饰器的情况下该怎么调用呢

    import time
    import random
    
    def timmer(func):
        def inner():
            start_time=time.time()
            func()
            end_time=time.time()
            print("run time: %s " %(end_time-start_time))
        return inner
    
    def auth(func):
        def wrapper():
            while True:
                user=input("Input your username>>>:").strip()
                pwd=input("Input your password>>>:").strip()
                if user== "abcd" and pwd == "abcd1234":
                    print("login successful")
                    func()
                    break
                else:
                    print("login error")
        return wrapper
    
    @timmer
    @auth
    def index():
        time.sleep(2)
        print("welcome to index page")
    
    index()

在上面的代码里,为index函数添加了两个装饰器,现在有一个问题,就是这两个装饰器究竟哪个先被调用,哪个后被调用呢??

来分析一下,

如果timmer装饰器先被调用,那么程序就会先执行timmer装饰器,然后再执行auth装饰器,提示输入用户名和密码,
    这样一来timmer装饰器统计的时间就会包括输入用户名和密码的时间,这个时间会远远大于index函数睡眠的2秒种;
如果auth装饰器先被调用,timmer装饰器后被调用,那么timmer装饰器统计的运行时间就应该只包括index函数的执行时间值应该在2秒多一点点的时间范围内

运行程序,先输入错误的用户名和密码以使用程序的执行时间加长

Input your username>>>:abcd
Input your password>>>:abcd
login error
Input your username>>>:abcd
Input your password>>>:abcd1234
login successful
welcome to index page
run time: 12.759000062942505 

从程序的执行结果可以知道,程序是先运行timmer装饰器,然后才运行auth装饰器,所以timmer统计的时间就包括了用户认证的时间,所以timmer统计到的程序运行时间远远大于index睡眠的2秒钟

所以这里得出一个结论:

当一个函数同时被两个装饰器装饰时,加上函数最上面的装饰器先执行,加在下面的装饰器先装饰

把上面例子里的timmer装饰器和auth装饰器位置互换一下

    import time
    import random
    
    def timmer(func):
        def inner():
            start_time=time.time()
            func()
            end_time=time.time()
            print("run time: %s " %(end_time-start_time))
        return inner
    
    def auth(func):
        def wrapper():
            while True:
                user=input("Input your username>>>:").strip()
                pwd=input("Input your password>>>:").strip()
                if user== "abcd" and pwd == "abcd1234":
                    print("login successful")
                    func()
                    break
                else:
                    print("login error")
        return wrapper
    
    @auth
    @timmer
    def index():
        time.sleep(2)
        print("welcome to index page")
    
    index()

运行index函数,依然先输入错误的用户名和密码,增加用户认证的时间

Input your username>>>:abcd
Input your password>>>:abcd
login error
Input your username>>>:abcd
Input your password>>>:abcd1234
login successful
welcome to index page
run time: 2.0 

可以看到,这次timmer统计到的时间只包含index函数的运行时间,不包含用户进行认证的时间

来分析一下上面例子中,index函数被timmer装饰器和auth装饰器装饰的代码装饰流程

    @auth           # index=auth(timmer(index))
    @timmer         # index=timmer(index)
    def index():
        time.sleep(2)
        print("welcome to index page")

在上面得出结论,一个函数同时被两个装饰器时,加在下面的装饰器先装饰

1.timmer装饰器装饰原始的index,可以写成:index=timmer(index)
2.在timmer装饰器中,timmer装饰器实际上是返回inner的内存地址,所以在这里,index=inner
3.timmer装饰器装饰完成后,由auth装饰器来装饰,此时可以写成index=auth(index),
4.这里auth括号里的index已经不再是原始index函数,而是已经被timmer装饰过后的index了,所以index=auth(timmer(index))
5.又因为timmer装饰的结果等于inner函数的内存地址,所以:index=auth(inner)

至此,两个装饰器的装饰过程已经知道了,来看程序的执行过程

6.程序先执行auth装饰器,进入用户认证,请用户输入用户名和密码
7.用户输入正确的用户名和密码后,开始执行func函数,也已经上面分析的inner函数
8.timmer装饰器先定义程序的开始运行时间,然后运行func函数,也就是原生的index函数
9.index函数先睡眠2秒,然后执行print语句,再定义程序的结束时间
10.最后统计并打印程序的运行时间,至此程序运行完毕。

所以这里用户输入用户名和密码的时间不会被timmer装饰器统计在内

7.被装饰函数参数的设置与定义

先来看一段代码

    import time
    
    def timmer(func):
        def inner():
            start_time=time.time()
            func()
            end_time=time.time()
            print("run time: %s " %(end_time-start_time))
        return inner
    
    @timmer
    def index():
        time.sleep(2)
        print("welcome to index page")
    
    @timmer
    def home(name):
        time.sleep(3)
        print("welcome to %s home page" % name)

如上所示,home函数添加了一个参数,而index函数并没有参数

按照正常的函数的定义与调用方式,调用index函数和home函数的方式应该是下面这种形式

index()
home("python")

然后我们运行程序就会发现,程序抛出了异常

  File "E:\python_learn\py_code\test.py", line 28, in <module>
    home("python")
TypeError: inner() takes 0 positional arguments but 1 was given

说个异常说明inner函数不需要位置参数,但是我们给了一个位置参数

回到timmer装饰器定义的部分,可以看到,timmer装饰器的内部函数确实没有定义参数

这样一来,timmer装饰器只能用于装饰没有参数的函数了,

我们可以在timmer装饰器定义的时候为inner函数添加一个参数

    import time
    
    def timmer(func):
        def inner(name):
            start_time=time.time()
            func(name)
            end_time=time.time()
            print("run time: %s " %(end_time-start_time))
        return inner
    
    @timmer
    def index():
        time.sleep(2)
        print("welcome to index page")
    
    @timmer
    def home(name):
        time.sleep(3)
        print("welcome to %s home page" % name)
    
    index()
    home("python")

但是这样一来,timmer装饰器装饰index函数的时候又会抛出异常,因为index函数没有参数

File "E:\python_learn\py_code\test.py", line 27, in <module>
index()
TypeError: inner() missing 1 required positional argument: ‘name‘       

在不知道被装饰函数的参数个数的情况下,即被装饰函数的参数可变长,且形式不固定的时候,

可以使用*args**kwargs,把上面的代码修改

    import time
    
    def timmer(func):
        def inner(*args,**kwargs):
            start_time=time.time()
            func(*args,**kwargs)
            end_time=time.time()
            print("run time: %s " %(end_time-start_time))
        return inner
    
    @timmer
    def index():
        time.sleep(2)
        print("welcome to index page")
    
    @timmer
    def home(name):
        time.sleep(3)
        print("welcome to %s home page" % name)
    
    index()
    home("python")

再次运行程序,查看运行结果

welcome to index page
run time: 2.0 
welcome to python home page
run time: 3.0 

由上可知,在不知道被装饰函数的参数个数时,可以使用*args**kwargs来表示任意长度任意形式的参数

8.被装饰函数的返回值

修改上面的代码,为home函数定义一个返回值,分别打印index函数和home函数的返回值

    import time
    
    def timmer(func):
        def inner(*args,**kwargs):
            start_time=time.time()
            func(*args,**kwargs)
            end_time=time.time()
            print("run time: %s " %(end_time-start_time))
        return inner
    
    @timmer
    def index():
        time.sleep(2)
        print("welcome to index page")
    
    @timmer
    def home(name):
        time.sleep(3)
        print("welcome to %s home page" % name)
        return("home func")
    
    index_res=index()
    print(index_res)
    
    home_res=home("python")
    print(home_res)

运行程序,可以看到

welcome to index page
run time: 2.0 
None
welcome to python home page
run time: 3.0 
None

可以看到,home函数中定义的返回值并没有被打印出来,显示的值为None

因为这里执行的home函数不是原始定义的home函数,而是wrapper函数的执行结果

因为wrapper函数并没有定义返回值,所以执行被装饰后的home函数并没有打印出返回值

修改代码,在timmer装饰器中定义并返回被装饰函数执行的返回值

    import time
    
    def timmer(func):
        def inner(*args,**kwargs):
            start_time=time.time()
            res=func(*args,**kwargs)
            end_time=time.time()
            print("run time: %s " %(end_time-start_time))
            return res
        return inner
    
    @timmer
    def index():
        time.sleep(2)
        print("welcome to index page")
    
    @timmer
    def home(name):
        time.sleep(3)
        print("welcome to %s home page" % name)
        return("home func")
    
    index_res=index()
    print(index_res)
    
    home_res=home("python")
    print(home_res)

再次执行函数,查看执行结果

    welcome to index page
    run time: 2.0 
    None
    
    welcome to python home page
    run time: 3.0 
    home func
    

可以看来,原始home函数中定义的返回值被打印出来了

结论:

如果被装饰函数没有定义返回值,timmer装饰器装饰后的返回值为None
而如果被装饰函数定义了返回值,则timmer装饰器装饰后则返回被装饰函数的返回值

以上是关于python函数式编程之装饰器的主要内容,如果未能解决你的问题,请参考以下文章

Python之装饰器简介

Python实用笔记 (15)函数式编程——装饰器

python中的函数式编程与装饰器

2.1python高级编程1-函数式编程和装饰器

Go的魅力, 函数式(柯里化, 闭包, 高阶函数), Python@装饰器, 封装

Python 函数式编程装饰器以及一些相关概念简介