6. 装饰器

装饰器说白了, 就是一个加工函数的函数. 我们通常用的 @ 操作符是一个语法糖. Listing 6.1Listing 6.2 的含义是相同的.

Listing 6.1 examples/object/decorator/demo_1.py
@decorator
def function():
    pass
Listing 6.2 examples/object/decorator/demo_2.py
def function():
    pass

function = decorator(function)

Listing 6.1Listing 6.2 反汇编代码如 Python Disassembly 6.1Python Disassembly 6.2 所示, 可以看出, 二者都是通过 CALL_FUNCTION 调用函数 decorator 修饰器, 并且通过 STORE_NAMEdecorator 的返回值保存到 function 中.

Python Disassembly 6.1 examples/object/decorator/demo_1.py
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
Python Disassembly 6.2 examples/object/decorator/demo_2.py
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 所示.

Listing 6.3 examples/object/decorator/time_decorator.py
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)}')
Bash 6.1 examples/object/decorator/time_decorator.py 执行结果
$ 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 所示.

Listing 6.4 examples/object/decorator/time_decorator_with_arguments.py
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)}')
Bash 6.2 examples/object/decorator/time_decorator_with_arguments.py 执行结果
$ 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 所示.

Python Disassembly 6.3 examples/object/decorator/time_decorator_with_arguments.py
 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.4Python 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_CLOSURELOAD_DEREF 的解释.

  • LOAD_CLOSURE(i): Pushes a reference to the cell contained in slot i of the cell and free variable storage. The name of the variable is co_cellvars[i] if i is less than the length of co_cellvars. Otherwise it is co_freevars[i - len(co_cellvars)].

  • LOAD_DEREF(i): Loads the cell contained in slot i of the cell and free variable storage. Pushes a reference to the object the cell contains on the stack.

修饰器中的参数或者状态变量, 会通过闭包的方式逐层的传递到内层的函数, 从而解决变量生命周期提前结束的问题. 事实上, 如果某个函数通过闭包访问外部的变量, 那么这个变量会保存在这个函数的 __closure__ 对象内, 如 Listing 6.5 所示.

Listing 6.5 examples/object/decorator/print_closure.py
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__]}')

其运行结果如下所示, 根据运行结果, 我们可以得到几个结论:

Bash 6.3 examples/object/decorator/print_closure.py 执行结果
$ 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 所示.

Listing 6.6 examples/object/decorator/time_decorator_with_arguments_v2.py
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)}')
Bash 6.4 examples/object/decorator/time_decorator_with_arguments_v2.py 执行结果
$ 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 中的类名规范是大驼峰, 但是装饰器名称一般都是小写, 在标准上存在冲突..