装饰器

装饰器:本质上就是函数,是高阶函数,是对传入函数功能的装饰(功能增强)

  装饰器就是一个以函数作为参数并返回一个函数可执行函数。本质上就是一个函数,该函数用来处理其他函数它可以让其他函数在不需要做任何代码变动的前提下增加额外功能,装饰器的返回值也是一个函数对象。它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。

器:指函数。

装饰:即修饰其他函数,为其他函数添加附加功能。

看一个例子:
>>> def foo(fun):    # 函数传递
...     def wrap():    # 嵌套函数
...         print('start...')
...         fun()
...         print('end...')
...         print(fun.__name__)
...     return wrap
...
>>> def bar():
...     print('I am in bar...')
...
>>> f = foo(bar)
>>> f()
start...
I am in bar...
end...
bar
    改写上面的代码:
>>> def foo(fun):
...     def wrap():
...         print('start...')
...         fun()
...         print('end...')
...         print(fun.__name__)
...     return wrap
...
>>> @foo    # 定义装饰器函数(语法糖)
... def bar():    # 被装饰器装饰的函数
...     print('I am in bar...')
...
>>> bar()    # 直接执行bar函数得到和上面相同的结果
start...
I am in bar...
end...
bar

以上就是所谓的装饰器及其应用,foo()就是装饰器函数,@foo用来装饰bar()函数。

装饰器本身是一个函数,将被装饰的类或者函数当作参数传递给装饰器函数。

装饰器详解:

  装饰器原则:

    不修改被修饰函数的源代码;

    不修改被修饰函数的调用方式。

      装饰器 = 高阶函数 + 函数嵌套 + 闭包

  为foo函数增加计算其运行时间的功能。

  高阶函数的支持:

函数接收的参数是一个函数名的方式
>>> import time
>>> def test(func):    # 函数接收的参数是一个函数名的方式
...     start_time = time.time()
...     func()
...     stop_time = time.time()
...     print('函数的运行时间为:%s'%(stop_time-start_time))
...
>>> def foo():    # 原函数没有被修改
...     time.sleep(3)
...     print('from the foo...')
...
>>> test(foo)    # 原函数的调用方式被修改了
from the foo...
函数的运行时间为:3.002479076385498

  只通过函数接收的参数是一个函数名的方式来实现装饰功能,可以满足不修改原函数代码,但不能满足不修改原函数的调用方式,即这种方式修改了原函数的调用方式。

函数返回值是函数名的方式
>>> import time
>>> def foo(): # 原函数没有被修改
... time.sleep(3)
... print('from the foo...')
...
>>> def test(func):
... start_time = time.time()
... func()
... stop_time = time.time()
... print('函数的运行时间为:%s'%(stop_time-start_time))
... return func # 返回值是一个函数名
...
>>> foo = test(foo) # 以原函数名的方式接收返回值,保证了不会改变原函数的调用方式
from the foo... # 但是会先输出调用函数中的内容
函数的运行时间为:3.0039541721343994
>>> foo() # 不改变原函数的调用方式
from the foo... # 原函数又被调用一次,即原函数会被调用两次

  通过高阶函数(参数是一个函数名、返回值是一个函数名)的方式,最终可以达到不修改原函数的代码,不改变原函数的调用方式,满足了装饰器的两个原则,但是却会将原函数调用两次,并且还有一个以原函数名接收装饰器返回值的赋值操作,没有完全满足装饰器的要求。
  高阶函数部分对装饰器的应用效果到此为止,但还没有完全实现装饰器,接下来看函数嵌套和闭包对装饰器的应用。
  函数嵌套和闭包的支持:

>>> import time
>>> def foo():    # 原函数没有被改变
...     time.sleep(3)
...     print('from the foo...')
...
>>> def test(func):    # 高阶函数支持,参数为函数
...     def wrapper():    # 函数嵌套支持
...         start_time = time.time()    # 为原函数添加统计执行时间的功能
...         func()    # 调用原函数,原函数在上层函数中,闭包的支持
...         stop_time = time.time()
...         print('函数执行时间:%s'%(stop_time-start_time))
...     return wrapper    # 高阶函数支持,返回函数
...
>>> foo = test(foo)    # 赋值操作,以原函数名接收返回值
>>> foo()    # 原函数调用方式没有改变
from the foo...
函数执行时间:3.004521131515503

  到目前为止,高阶函数、函数嵌套和闭包所支持的装饰器函数已完成绝大部分功能。上面定义的函数test()即为装饰器,函数foo()即为被装饰的函数。现在只剩下最后一个问题:在以原函数名调用原函数(即不改变函数调用方式)时,先要执行一次接收装饰器函数返回值的赋值操作。
  为了解决在函数调用前先执行一次返回值的赋值操作,引入语法糖@符号。用法是:@装饰器函数(@test)。在被装饰的函数前面加上语法糖就相当于执行了接收装饰器函数返回值的赋值操作。就上面的函数而言@test就等于foo = test(foo)。

>>> import time
>>> def test(func):    
...     def wrapper():    
...         start_time = time.time()    
...         func()    
...         stop_time = time.time()
...         print('函数执行时间:%s'%(stop_time-start_time))
...     return wrapper    
...
>>> @test    # 添加语法糖,相当于上面的返回值赋值操作,即相当于执行了foo = test(foo)
... def foo():
...     time.sleep(3)
...     print('from the foo...')
...
>>> foo()    # 有了语法糖之后,python自动执行了返回值赋值操作,这里就直接调用函数即可
from the foo...
函数执行时间:3.004521131515503

  至此,在高阶函数、函数嵌套、闭包和语法糖的支持下,完整的完成了装饰器函数的定义与使用,装饰器的基本操作到此完成。

 

闭包支持下的装饰器函数的扩充:

  被装饰的函数即原函数中有返回值

    有了语法糖之后,python自动执行函数返回值赋值操作,最后执行的原函数名其实执行的不是真正的原函数,而是装饰器函数的返回值即其中定义的嵌套函数。若此时原函数中有返回值,将无法接收到原函数的返回值,因为装饰器函数中不能返回原函数的返回值,它只能返回嵌套函数。

>>> import time
>>> def test(func):    
...     def wrapper():    
...         start_time = time.time()    
...         func()    
...         stop_time = time.time()
...         print('函数执行时间:%s'%(stop_time-start_time))
...     return wrapper    
...
>>> @test
... def foo():
...     time.sleep(3)
...     print('from the foo...')
...     return '这是原函数的返回值...'    # 原函数中有返回值
...
>>>res = foo()    # 接收原函数的返回值
from the foo...
函数执行时间:3.0040957927703857
>>> print(res)    # 并没有接收到原函数的返回值
None

    这是因为虽然执行的是原函数名,但实际上执行的是装饰器函数中的嵌套函数wrapper,而wrapper中并没有接收原函数的返回值,也没用将其返回,即wrapper函数没有提供返回值,所以才返回了None。

>>> import time
>>> def test(func):
...     def wrapper():
...         start_time = time.time()
...         res = func()    # 嵌套函数wrapper中接收原函数的返回值
...         stop_time = time.time()
...         print('函数执行时间:%s'%(stop_time-start_time))
...         return res    # 嵌套函数wrapper中返回原函数的返回值
...     return wrapper
...
>>> @test
... def foo():
...     time.sleep(3)
...     print('from the foo...')
...     return '这是函数foo的返回值...'
...
>>> res = foo()    # 接收嵌套函数的返回值,即原函数的返回值
from the foo...
函数执行时间:3.004549741744995
>>> print(res)    # 打印出了原函数的返回值
这是函数foo的返回值...

  被装饰的函数即原函数中有参数

    当被修饰的函数即原函数中有参数时,装饰器函数中的嵌套函数wrapper函数也必须能够接收这些参数,因为执行原函数名调用实际上调用的是装饰器中的嵌套函数。而一个装饰器函数定义好之后,它应该能够为多个不同的函数增加相同的功能,也即它应该要能够修饰多个不同的函数,所以在定义装饰器函数的嵌套函数wrapper的参数时,应该用收集参数的方式去定义它。

>>> import time
>>> def test(func):
...     def wrapper(*args, **kwargs):    # 以收集参数的方式接收参数
...         start_time = time.time()
...         res = func(*args, **kwargs)    # 参数传给被修饰的函数
...         stop_time = time.time()
...         print('函数运行时间:%s'%(stop_time-start_time))
...         return res
...     return wrapper
...
>>> @test
... def foo(name, age, sex, x, y):    # 被修饰的函数有参数
...     print('name is %s, age is %s, sex is %s'%(name, age, sex))
...     print('x = %s, y = %s'%(x, y))
...     time.sleep(3)
...     return '这是函数foo...'
...
>>> @test
... def bar(x, y, z):    # 被修饰的函数有参数
...     print('x + y = ', x+y)
...     print('z = ', z)
...     time.sleep(2)
...
>>> foo('yang', 22, 'woman', x = 25, y = 33)
name is yang, age is 22, sex is woman
x = 25, y = 33
函数运行时间:3.003525733947754
'这是函数foo...'
>>> bar(3, 6, z=99)    # 被修饰的函数的参数不定长
x + y =  9
z =  99
函数运行时间:2.0029187202453613

有参装饰器:
  装饰器本身也可以支持参数,如果装饰器本身需要支持参数,那么装饰器就需要多一层的内嵌函数。
  带有参数的装饰器能够起到在运行时,有不同的功能。
  看下面的一个例子:通过装饰器对一个函数增加计时的功能,要求是当装饰器传入的状态为True时,为函数计时,若传入的状态为False时,直接执行原函数,不用计时。

>>> import time
>>> def state(flag=True):    # 装饰器带参数
...     def test(func):    # 嵌套函数作装饰器
...         if flag == True:    # 判断装饰器的状态,实现不同的功能
...             def wrapper(*args, **kwargs):    # 状态为True时,计时
...                 start_time = time.time()
...                 res = func(*args, **kwargs)
...                 stop_time = time.time()
...                 print('函数执行时间为:', stop_time-start_time)
...                 return res    # 返回被修饰函数的结果
...             return wrapper    
...         else:
...             return func    # 状态为False时,直接执行被修饰的函数,不计时
...     return test    # 返回装饰器本身
...
>>> @state(flag=True)    # 添加计时功能,函数名后面有()即调用了该函数
... def foo(x, y, z):
...     time.sleep(2)
...     print('x + y = ', x+y)
...     print('z = ', z)
...     return '增加了计时功能!'
...
>>> res = foo(2, 15, 'python')
x + y =  17
z =  python
函数执行时间为: 2.0031473636627197
>>> print(res)
'增加了计时功能!'
>>> @state(flag=False)    # 不计时
... def foo(x, y, z):
...     time.sleep(2)
...     print('x + y = ', x+y)
...     print('z = ', z)
...     return '直接执行函数,没有增加计时功能!'
...
>>> res = foo(3, 99, 'c')
x + y =  102
z =  c
>>> print(res)
直接执行函数,没有增加计时功能

多个装饰器:
  装饰器调用顺序:
    装饰器是可以叠加使用的,那么就涉及到装饰器调用顺序了。对于Python中的”@”语法糖,装饰器的调用顺序与使用 @ 语法糖声明的顺序相反。
    多个装饰器调用时,从下往上调用;执行时,从上往下执行。

>>> def test1(func):
...     print('这是装饰器1...')
...     def wrapper():
...         print('这是装饰器1的嵌套函数...')
...         func()
...         print('装饰器1的嵌套函数结束!')
...     print('装饰器1结束!')
...     return wrapper
...
>>> def test2(func):
...     print('这是装饰器2...')
...     def wrapper():
...         print('这是装饰器2的嵌套函数...')
...         func()
...         print('装饰器2的嵌套函数结束!')
...     print('装饰器2结束!')
...     return wrapper
...
>>> def test3(func):
...     print('这是装饰器3...')
...     def wrapper():
...         print('这是装饰器3的嵌套函数...')
...         func()
...         print('装饰器3的嵌套函数结束!')
...     print('装饰器3结束!')
...     return wrapper
...
>>> @test1
... @test2
... @test3
... def foo():
...     print('from the foo...')
...
这是装饰器3...    # 被装饰的函数定义好之后,就会先直接执行装饰器,从下往上执行装饰器
装饰器3结束!
这是装饰器2...
装饰器2结束!
这是装饰器1...
装饰器1结束!
>>> foo()    # 调用被装饰的函数(原函数)
这是装饰器1的嵌套函数...    # 行上往下执行装饰器内的嵌套函数,依次为原函数增加功能
这是装饰器2的嵌套函数...
这是装饰器3的嵌套函数...
from the foo...  # 虽然每个装饰器中都执行了原函数,但多个装饰器装饰一个函数时,被装饰的函数只执行一次
装饰器3的嵌套函数结束!    # 装饰器中的嵌套函数中的被装饰的原函数调用结束后
装饰器2的嵌套函数结束!    # 装饰器中的嵌套函数的执行顺序是从下往上执行
装饰器1的嵌套函数结束!    # 与嵌套函数中原函数调用前,嵌套函数的执行顺序对称(相反)

 

python装饰器库:
  先看一个简单的例子,了解一下functools.wraps()。

>>> def test(func):
...     def wrapper():
...         x = 3
...         print('x = ', x)
...         func()
...     return wrapper
...
>>> @test
... def foo():
...     print('from the foo...')
...
>>> foo()
x =  3
from the foo...
>>> foo.__name__
'wrapper'

  上述代码最后执行的结果不是被装饰的函数foo,而是装饰器中嵌套函数wrapper。这表示被装饰函数自身的信息丢失了,函数名指向了嵌套函数,怎么才能避免这种问题的发生呢?
  可以借助functools.wraps()函数:

>>> from functools import wraps    
>>> def test(func):
...     @wraps(func)
...     def wrapper():
...         func()
...     return wrapper
...
>>> @test
... def foo():
...     print('from the foo...')
...
>>> foo()
from the foo...
>>> foo.__name__    # 打印的是被装饰的函数,不再是装饰器的嵌套函数了
'foo'

functools.wraps的作用:
  上面已经通过了一个简单的例子,我们知道对于普通的装饰器,会丢失掉被装饰函数的内容,即被装饰函数被装饰器修改了。这是程序设计中一个很严重的问题,会影响测试环节。正如上面介绍的,解决这个问题的办法就是使用functools.wraps(func)装饰器。在编写装饰器时,在实现前加入 @functools.wraps(func) 可以保证装饰器不会对被装饰函数造成影响。

>>> from functools import wraps
>>> def test(func):
...     def wrapper():
...         'This is the function of wrapper'    # wrapper函数中的文档
...         func()
...     return wrapper
...
>>> @test
... def foo():
...     'This is the function of foo'    # foo函数中的文档
...     print('foo函数')
...
>>> foo()
foo函数
>>> foo.__name__    # 打印的是wrapper函数名
'wrapper'
>>> foo.__doc__    # 打印的是wrapper函数文档
'This is the function of wrapper'

>>> def test(func):
...     @wraps(func)    # functools的wraps装饰器
...     def wrapper():
...         'This is the function of wrapper'
...         func()
...     return wrapper
...
>>> @test
... def foo():
...     'This is the function of foo'
...     print('foo函数')
...
>>> foo()
foo函数
>>> foo.__name__    # 打印的是foo函数名
'foo'
>>> foo.__doc__    # 打印的是foo函数文档
'This is the function of foo'

  以上使用了functools.wraps(func)装饰器解决了普通装饰器会丢掉被装饰函数的内容,而改为装饰器嵌套函数内容的问题,使得被装饰函数的内容得以保留而不会被装饰器修改。

 

优化装饰器:
  嵌套的装饰函数不太直观,可以使用第三方包类改进这样的情况,让装饰器函数可读性更好。
  decorator.py
  decorator.py 模块是python用来专门封装装饰器的模块,使用decorator构造装饰器更加简便,同时被装饰的函数签名也保留不变。它是一个非常简单的装饰器加强包,可以很直观的先定义包装函数wrapper(),再使用decorate(func, wrapper)方法就可以完成一个装饰器。

无参装饰器:
>>> from decorator import decorator
>>> @decorator    # 使用decorator装饰器
... def wrapper(func, *args, **kwargs):    # 省去了外层装饰器函数的定义
...     print('使用decorator简化装饰器定义') # 将被装饰的函数直接传给简化的装饰器
...     func(*args, **kwargs)
...
>>> @wrapper    #装饰器函数
... def foo(x, y, z):
...     print('x - y=', x-y)
...     print('z =', z)
...
>>> foo(10, 5, 'python')
使用decorator简化装饰器定义
x - y= 5
z = python
>>> foo.__name__    # decorator.py实现的装饰器能完整保留原函数的内容
'foo'
        带参装饰器:
from decorator import decorator
>>> def state(flag=True):    # 装饰器带有参数
...     @decorator
...     def wrapper(func, *args, **kwargs):
...         print('带参数的简化装饰器')
...         if flag == True:
...             print('增加附加功能...')
...             func(*args, **kwargs)
...             print('end...')
...         else:
...             func(*args, **kwargs)
...     return wrapper
...
>>> @state(flag=True)
... def foo(x, y, z):
...     print('x + y =', x+y)
...     print('z =', z)
...
>>> foo(1, 5, 'python')
带参数的简化装饰器
增加附加功能...
x + y = 6
z = python
end...
>>> @state(flag=False)
... def foo(x, y, z):
...     print('x + y =', x+y)
...     print('z =', z)
...
>>> foo(1, 5, 'python')
带参数的简化装饰器
x + y = 6
z = python

  decorator.py实现的装饰器能完整保留原函数的name,doc和args,唯一有问题的就是inspect.getsource(func)返回的还是装饰器的源代码,你需要改成inspect.getsource(func.__wrapped__)。

wrapt:
  wrapt是一个功能非常完善的包,用于实现各种装饰器。使用wrapt实现的装饰器,不需要担心之前inspect中遇到的所有问题,因为它都帮你处理了,甚至inspect.getsource(func)也准确无误。
  无参装饰器:

from wrapt import decorator
>>> @decorator
... def wrapper(func, instance, args, kwargs):
...     print('使用wrapt简化装饰器定义')
...     func(*args, **kwargs)
...     print(func.__name__)
...
>>> @wrapper
... def foo(x, y):
...     print('x + y =', x+y)
...
>>> foo(9, 12)
使用wrapt简化装饰器定义
x + y = 21
foo
>>> foo.__name__
'foo'

  使用wrapt只需要定义一个装饰器函数,但是函数签名是固定的,必须是(func, instance, args, kwargs),注意第二个参数instance是必须的,就算不用它。当装饰器装饰在不同位置时它将得到不同的值,比如装饰在类实例方法时你可以拿到这个类实例。根据instance的值,能够更加灵活的调整装饰器。另外,args和kwargs也是固定的,注意前面没有星号。在装饰器内部调用原函数时才带星号。
  带参的装饰器的定义方法和上面的decorator.py模块类似。

posted @ 2018-11-13 15:19  从python开始  阅读(256)  评论(0编辑  收藏  举报