装饰器的语法为 @dec_name , 置于函数定义之前. 如:
- import atexit
- @atexit.register
- def goodbye():
- print('Goodbye!')
- print('Script end here')
atexit.register 是一个装饰器, 它的作用是将被装饰的函数注册为在程序结束时执行. 函数 goodbye 是被装饰的函数.
程序的运行结果是:
- Script end here
- Goodbye!
可见函数 goodbye 在程序结束后被自动调用.
另一个常见的例子是 @property , 装饰类的成员函数, 将其转换为一个描述符.
- class Foo:
- @property
- def attr(self):
- print('attr called')
- return 'attr value'
- foo = Foo()
等价语法
语句块
- @atexit.register
- def goodbye():
- print('Goodbye!')
等价于
- def goodbye():
- print('Goodbye!')
- goodbye = atexit.register(goodbye)
这两种写法在作用上完全等价.
从第二种写法, 更容易看出装饰器的原理. 装饰器实际上是一个函数 (或 callable), 其输入, 返回值为:
说明 | 示例中的对应 | |
---|---|---|
输入 | 被装饰的函数 | goodbye |
返回值 | 变换后的函数或任意对象 |
返回值会被赋值给原来指向输入函数的变量, 如示例中的 goodbye . 此时变量 goodbye 将指向装饰器的返回值, 而不是原来的函数定义. 返回值一般为一个函数, 这个函数是在输入参数函数添加了一些额外操作的版本.
如下面这个装饰器对原始函数添加了一个操作: 每次调用这个函数时, 打印函数的输入参数及返回值.
- def trace(func):
- def wrapper(*args, **kwargs): 1
- print('Enter. Args: %s, kwargs: %s' % (args, kwargs)) 2
- rv = func(*args, **kwargs) 3
- print('Exit. Return value: %s' % (rv)) 4
- return rv
- return wrapper
- @trace
- def area(height, width):
- print('area called')
- return height * width
- area(2, 3) 5
1 : 定义一个新函数, 这个函数将作为装饰器的返回值, 来替换原函数
2, 4 : 打印输入参数, 返回值. 这是这个装饰器所定义的操作
3 : 调用原函数
5 : 此时 area 实际上是 1 处定义的 wrapper 函数
程序的运行结果为:
- Enter. Args: (2, 3), kwargs: {}
- area called
- Exit. Return value: 6
如果不使用装饰器, 则必须将以上打印输入参数及返回值的语句直接写在 area 函数里, 如:
- def area(height, width):
- print('Enter. Args: %s, %s' % (height, width))
- print('area called')
- rv = height * width
- print('Exit. Return value: %s' % (rv))
- return rv
- area(2, 3)
程序的运行结果与使用装饰器时相同. 但使用装饰器的好处为:
打印输入参数及返回值这个操作可被重用
如对于一个新的函数 foo , 装饰器 trace 可以直接拿来使用, 而无须在函数内部重复写两条 print 语句.
- @trace
- def foo(val):
- return 'return value'
一个装饰器实际上定义了一种可重复使用的操作
函数的功能更单纯
area 函数的功能是计算面积, 而调试语句与其功能无关. 使用装饰器可以将与函数功能无关的语句提取出来. 因此函数可以写地更小.
使用装饰器, 相当于将两个小函数组合起来, 组成功能更强大的函数
修正名称
以上例子中有一个缺陷, 函数 area 被 trace 装饰后, 其名称变为 wrapper , 而非 area . print(area) 的结果为:
<function wrapper at 0x10df45668>
wrapper 这个名称来源于 trace 中定义的 wrapper 函数.
可以通过 functools.wraps 来修正这个问题.
- from functools import wraps
- def trace(func):
- @wraps(func)
- def wrapper(*args, **kwargs):
- print('Enter. Args: %s, kwargs: %s' % (args, kwargs))
- rv = func(*args, **kwargs)
- print('Exit. Return value: %s' % (rv))
- return rv
- return wrapper
- @trace
- def area(height, width):
- print('area called')
- return height * width
即使用 functools.wraps 来装饰 wrapper . 此时 print(area) 的结果为:
<function area at 0x10e8371b8>
函数的名称能够正确显示.
接收参数
以上例子中 trace 这个装饰器在使用时不接受参数. 如果想传入参数, 如传入被装饰函数的名称, 可以这么做:
- from functools import wraps
- def trace(name):
- def wrapper(func):
- @wraps(func)
- def wrapped(*args, **kwargs):
- print('Enter %s. Args: %s, kwargs: %s' % (name, args, kwargs))
- rv = func(*args, **kwargs)
- print('Exit %s. Return value: %s' % (name, rv))
- return rv
- return wrapped
- return wrapper
- @trace('area')
- def area(height, width):
- print('area called')
- return height * width
- area(2, 3)
程序的运行结果为:
- Enter area. Args: (2, 3), kwargs: {}
- area called
- Exit area. Return value: 6
将函数名称传入后, 在日志同时打印出函数名, 日志更加清晰.
@trace('area') 是如何工作的?
这里其实包含了两个步骤. @trace('area') 等价于:
- dec = trace('area')
- @dec
- def area(height, width): ...
即先触发函数调用 trace('area') , 得到一个返回值, 这个返回值为 wrapper 函数. 而这个函数才是真正的装饰器, 然后使用这个装饰器装饰函数.
多重装饰器
装饰器可以叠加使用, 如:
- @dec1
- @dec2
- def foo():pass
等价的代码为:
- def foo():pass
- foo = dec2(foo)
- foo = dec1(foo)
即装饰器依次装饰函数, 靠近函数定义的装饰器优先. 相当于串联起来.
来源: https://www.cnblogs.com/astropeak/p/9029104.html