装饰器(decorator)是一个可以应用到函数或类上的 callable。 装饰器会接收它所装饰的对象,对它做一些处理, 然后返回一个结果;Python 会把这个结果重新绑定到原来的名字上。 大多数装饰器都用于函数,所以本章先从函数开始。

要应用装饰器, 就在定义上一行写 @,后面跟装饰器名字。 为了简单起见,第一个例子使用没有精确类型参数的 Callable:

# simple_decoration.py
from collections.abc import Callable

def hijack(func: Callable) -> Callable:
    def doesnt_matter() -> None:
        print("Replacement behavior")

    return doesnt_matter

@hijack
def cheese() -> None:
    print("Wensleydale")

cheese()
#: Replacement behavior

稍后的保持被包装对象的接口 会展示带完整类型的装饰器。

上面 cheese() 之前的 @hijack 等价于:

cheese = hijack(cheese)

hijack 返回 doesnt_matter,Python 随后把这个函数赋给名字 cheese, 所以现在 cheese 指向的是 doesnt_matter。 调用 cheese() 实际会运行 doesnt_matter, 它从不调用 func,而是打印自己的消息。 原始 cheese() 的函数体根本不会执行。

既然 Python 会把返回的函数绑定给 cheese 这个名字,内部局部函数叫什么 (这里是 doesnt_matter())其实无所谓。 惯例通常把它叫作 wrapper()。

典型装饰器会返回一个 wrapper:先做一些工作, 调用原函数,然后再做一些工作:

# typical_decorator.py
from collections.abc import Callable

def add_behavior(func: Callable) -> Callable:
    def wrapper() -> None:
        print("Some work")
        func()
        print("Some more work")

    return wrapper

@add_behavior
def cheese() -> None:
    print("Wensleydale")

cheese()
#: Some work
#: Wensleydale
#: Some more work

如果装饰器忘记写 return wrapper,那么 cheese 会被绑定为 None, 而错误要等到下一次调用 cheese() 时才暴露, 而不是在真正造成问题的装饰操作发生时暴露。

装饰器是在 Python 执行 def 时运行的, 不是在你调用被装饰函数时才运行:

# decoration_time.py
from collections.abc import Callable

def announce(func: Callable) -> Callable:
    print("Decorating")
    def wrapper() -> None:
        print("Calling")
        func()
    return wrapper

@announce
def cheese() -> None:
    print("Wensleydale")

print("Definitions done")
cheese()
#: Decorating
#: Definitions done
#: Calling
#: Wensleydale

Decorating 会在 Definitions done 之前打印, 所以 Python 仍在执行 cheese 上面的那个 def 时,announce 就已经运行了。 只有 wrapper() 的函数体会等到真正调用时才执行。

wrapper() 是一个闭包(closure)。 它定义在装饰器内部,并引用 func——这是来自外层作用域的变量,而不是它自己的参数。 只要 wrapper() 仍然存在,Python 就会保持 func 存活, 即使装饰器函数早已返回。 因此,即便装饰过程早就结束,之后很久才调用的 cheese() 仍然能通过 func 找到原来的 cheese 函数。 闭包会介绍这种通用机制。

装饰是一种简单的17 · 元编程。 同一个思想在设计模式中叫作 Decorator 模式: 用一个对象包装另一个对象,为它增加职责, 同时保持被包装对象原有接口,使使用它的代码感受不到包装层。

保持被包装对象的接口

到目前为止的 wrapper 都没有声明参数, 因此 add_behavior 只能作用于零参数函数。 如果把 add_behavior 用在 def add(a, b) 上,调用 add(2, 3) 就会抛出 TypeError: wrapper 只接收零个位置参数,而调用传了两个。 如果 wrapper 必须处理任意函数,就应该用 *args, **kwargs 收集调用参数,并原样转发, 这正是参数解包里的模式。 下面的装饰器会跟踪函数调用,其 wrapper() 就采用这种形状: