6. 装饰器
装饰器说白了, 就是一个加工函数的函数. 我们通常用的 @
操作符是一个语法糖. Listing 6.1 和 Listing 6.2 的含义是相同的.
@decorator
def function():
pass
def function():
pass
function = decorator(function)
Listing 6.1 和 Listing 6.2 反汇编代码如 Python Disassembly 6.1 和 Python Disassembly 6.2 所示, 可以看出, 二者都是通过 CALL_FUNCTION
调用函数 decorator
修饰器, 并且通过 STORE_NAME
将 decorator
的返回值保存到 function
中.
1 0 LOAD_NAME 0 (decorator)
2 2 LOAD_CONST 0 (<code object function at 0x7f45c9e6aea0, file "<disassembly>", line 1>)
4 LOAD_CONST 1 ('function')
6 MAKE_FUNCTION 0
8 CALL_FUNCTION 1
10 STORE_NAME 1 (function)
12 LOAD_CONST 2 (None)
14 RETURN_VALUE
1 0 LOAD_CONST 0 (<code object function at 0x7f45cafb1660, file "<disassembly>", line 1>)
2 LOAD_CONST 1 ('function')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (function)
4 8 LOAD_NAME 1 (decorator)
10 LOAD_NAME 0 (function)
12 CALL_FUNCTION 1
14 STORE_NAME 0 (function)
16 LOAD_CONST 2 (None)
18 RETURN_VALUE
装饰器有很多应用场景, 比如, 我想打印函数的执行时间, 那么可以定义一个装饰器, 如 Listing 6.3 所示.
from time import time
from functools import wraps
def print_time(function):
@wraps(function)
def wrapper(*args, **kwargs):
now = time()
return_value = function(*args, **kwargs)
print('consuming time is %fs' % (time() - now))
return return_value
return wrapper
@print_time
def add(x, y):
return x + y
print(f'add(1, 2) = {add(1, 2)}')
print(f'add(3, 2) = {add(3, 2)}')
$ python3 examples/object/decorator/time_decorator.py
consuming time is 0.000001s
add(1, 2) = 3
consuming time is 0.000000s
add(3, 2) = 5
通常情况下, 修饰器并不会这么简单, 而是会携带一些可配置的参数, 比如, 我们要在 Listing 6.3 的基础上添加一个消息模板的参数, 实现代码如 Listing 6.4 所示.
from time import time
from functools import wraps
def print_time(message):
def wrapper(function):
@wraps(function)
def _print_time(*args, **kwargs):
now = time()
return_value = function(*args, **kwargs)
print(message % (time() - now))
return return_value
return _print_time
return wrapper
@print_time(message='consuming time %fs')
def add(x, y):
return x + y
print(f'add(1, 2) = {add(1, 2)}')
print(f'add(3, 2) = {add(3, 2)}')
$ python3 examples/object/decorator/time_decorator_with_arguments.py
consuming time 0.000001s
add(1, 2) = 3
consuming time 0.000000s
add(3, 2) = 5
我们都知道, 在 Python 中, 局部变量的生命周期与所在函数的生命周期一致, 如果函数结束了, 那么局部变量的生命周期也结束了. 这个特性如果作用在装饰器上就会出现问题, 比如在 Listing 6.4 中, 函数 _print_time
用到了 message
变量, 这个变量是来自函数 print_time
的, 当调用 _print_time
(也就是 add
函数) 时, 函数 print_time
已经结束运行了, 按理说变量 message
也一同消失了, 但是根据执行结果可以看出在调用 _print_time
时变量 message
仍然存在.
那么 Python 是怎么解决这个问题的呢? 我们来看一下 Listing 6.4 的反汇编代码, 如 Python Disassembly 6.3 所示.
1 0 LOAD_CONST 0 (0)
2 LOAD_CONST 1 (('time',))
4 IMPORT_NAME 0 (time)
6 IMPORT_FROM 0 (time)
8 STORE_NAME 0 (time)
10 POP_TOP
2 12 LOAD_CONST 0 (0)
14 LOAD_CONST 2 (('wraps',))
16 IMPORT_NAME 1 (functools)
18 IMPORT_FROM 2 (wraps)
20 STORE_NAME 2 (wraps)
22 POP_TOP
4 24 LOAD_CONST 3 (<code object print_time at 0x7f45caf97920, file "<disassembly>", line 4>)
26 LOAD_CONST 4 ('print_time')
28 MAKE_FUNCTION 0
30 STORE_NAME 3 (print_time)
15 32 LOAD_NAME 3 (print_time)
34 LOAD_CONST 5 ('consuming time %fs')
36 LOAD_CONST 6 (('message',))
38 CALL_FUNCTION_KW 1
16 40 LOAD_CONST 7 (<code object add at 0x7f45ca77f190, file "<disassembly>", line 15>)
42 LOAD_CONST 8 ('add')
44 MAKE_FUNCTION 0
46 CALL_FUNCTION 1
48 STORE_NAME 4 (add)
19 50 LOAD_NAME 5 (print)
52 LOAD_CONST 9 ('add(1, 2) = ')
54 LOAD_NAME 4 (add)
56 LOAD_CONST 10 (1)
58 LOAD_CONST 11 (2)
60 CALL_FUNCTION 2
62 FORMAT_VALUE 0
64 BUILD_STRING 2
66 CALL_FUNCTION 1
68 POP_TOP
20 70 LOAD_NAME 5 (print)
72 LOAD_CONST 12 ('add(3, 2) = ')
74 LOAD_NAME 4 (add)
76 LOAD_CONST 13 (3)
78 LOAD_CONST 11 (2)
80 CALL_FUNCTION 2
82 FORMAT_VALUE 0
84 BUILD_STRING 2
86 CALL_FUNCTION 1
88 POP_TOP
90 LOAD_CONST 14 (None)
92 RETURN_VALUE
通过 Listing 6.4 和 Python Disassembly 6.3 对照, 我们可以看出:
在第 5 行, 在定义函数
wrapper
时, 执行LOAD_CLOSURE
将变量message
保存在函数wrapper
中.在第 7 行, 在定义函数
_print_time
时, 执行LOAD_CLOSURE
将变量message
保存在函数_print_time
中.在第 10 行, 在调用变量
message
时, 执行LOAD_DEREF
将变量message
加载回来.
Hint
在 Python 的官方文档中, 有关于 LOAD_CLOSURE
和 LOAD_DEREF
的解释.
LOAD_CLOSURE(i)
: Pushes a reference to the cell contained in sloti
of the cell and free variable storage. The name of the variable isco_cellvars[i]
ifi
is less than the length ofco_cellvars
. Otherwise it isco_freevars[i - len(co_cellvars)]
.LOAD_DEREF(i)
: Loads the cell contained in sloti
of the cell and free variable storage. Pushes a reference to the object the cell contains on the stack.
修饰器中的参数或者状态变量, 会通过闭包的方式逐层的传递到内层的函数, 从而解决变量生命周期提前结束的问题. 事实上, 如果某个函数通过闭包访问外部的变量, 那么这个变量会保存在这个函数的 __closure__
对象内, 如 Listing 6.5 所示.
def get_function():
a = 1
b = 2
def function():
return a, b
return function
f = get_function()
print(f'class of f.__closure__ is {f.__closure__.__class__}')
print(f'class of get_function.__closure__ is {get_function.__closure__.__class__}')
print(f'content of f.__closure__ is {[item.__class__ for item in f.__closure__]}')
print(f'value of f.__closure__ is {[item.cell_contents for item in f.__closure__]}')
其运行结果如下所示, 根据运行结果, 我们可以得到几个结论:
$ python3 examples/object/decorator/print_closure.py
class of f.__closure__ is <class 'tuple'>
class of get_function.__closure__ is <class 'NoneType'>
content of f.__closure__ is [<class 'cell'>, <class 'cell'>]
value of f.__closure__ is [1, 2]
如果一个函数存在闭包, 那么它的
__closure__
是一个tuple
类型, 否则__closure__
的值为None
.__closure__
中元素的类型是cell
, 元素个数为闭包变量的数量.__closure__
中元素的cell_contents
属性为闭包变量的值.
闭包的存在, 使得装饰器有了状态, 如果要实现一个有状态的装饰器不一定非要使用闭包. 这里可以介绍另一种实现有参数装饰器的方式, 如 Listing 6.6 所示.
from time import time
from functools import wraps
class print_time:
def __init__(self, message):
self.message = message
def __call__(self, function):
@wraps(function)
def wrapper(*args, **kwargs):
now = time()
return_value = function(*args, **kwargs)
print(self.message % (time() - now))
return return_value
return wrapper
@print_time(message='consuming time %fs')
def add(x, y):
return x + y
print(f'add(1, 2) = {add(1, 2)}')
print(f'add(3, 2) = {add(3, 2)}')
$ python3 examples/object/decorator/time_decorator_with_arguments_v2.py
consuming time 0.000001s
add(1, 2) = 3
consuming time 0.000001s
add(3, 2) = 5
Listing 6.6 这种方式理解起来比较简单, 参数再多也不会混乱, 但是也有一点的小缺点: Python 中的类名规范是大驼峰, 但是装饰器名称一般都是小写, 在标准上存在冲突..