装饰器的语法为 @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)
即装饰器依次装饰函数,靠近函数定义的装饰器优先。相当于串联起来。
如果你一路读到这里,我相信你已经掌握了关于Python 装饰器80%的知识,并能够应用到工作学习中。你知道了装饰器的工作原理,以及如何自己编写一个装饰器,及避免常见的编写错误。
如果你仍然有疑问,欢迎留言讨论!(如果你觉得这篇文章有用,请点下方推荐按钮:p)