装饰器是一个包裹函数的函数
这句话听起来很抽象,但原理很简单。装饰器接收一个函数,返回一个函数。它返回的函数通常会调用原来的函数,并在这次调用周围加上一些额外的行为。
最简短的例子:
shout 就是装饰器。它接收一个函数(greet),构建一个新函数(wrapper),新函数调用原函数并把结果转成大写,然后返回这个新函数。重新赋值 greet = shout(greet),就用包裹后的版本替换了原函数。
这种重新赋值的写法太常见了,所以 Python 专门为它设计了一种语法。
@ 是重新赋值的语法糖
在 def 上面一行写 @name,等价于在函数定义之后紧接着写 name = name(...):
@shout 读作“把 shout 装饰器应用到这个函数上”。Python 在 def 之后立即运行 greet = shout(greet):原理和之前一样,只是少打几个字。
以后看到 @name,就在脑子里把它替换成 function = name(function)。这个语法的全部意思就是这样。
处理参数
大多数函数都接收参数。一个可用的装饰器要把参数原样传下去。惯用写法是 *args, **kwargs,这是 Python 接收任意参数的方式,因为包装函数不应该关心被包裹的函数需要什么参数:
*args 捕获所有位置参数。**kwargs 捕获所有关键字参数。包装函数把所有参数原封不动地转发给被包裹的函数,然后做装饰器要做的额外工作,这里是把结果转成大写。
大多数真实的装饰器都是这个样子。
一个更有用的例子:计时
打印一个函数花了多长时间:
在调用之前运行点什么,在调用之后再运行点什么,这正是大多数装饰器最终在做的事。日志、权限检查、重试和输入验证都遵循同样的结构。
保留原函数的身份:functools.wraps
装饰一个函数会替换掉它,这意味着被包裹的函数会丢失原来的 __name__ 和 __doc__ 属性:
greet.__name__ 现在是 "wrapper",文档字符串也不见了。这会破坏 help()、traceback,以及任何检查这个函数的工具。
修复只需要一行:在内部函数上加 @functools.wraps(func),它会把元数据复制过来。
一定要给内部函数加上 @wraps(func)。它没有任何代价,还能让你以后免于在调试时碰到意外。
带参数的装饰器
有时候装饰器本身也需要配置,比如“这个函数最多重试 3 次”“以 DEBUG 级别记录日志”。这就意味着要再多嵌套一层:一个外层函数接收参数,并返回一个装饰器。
三层听起来很多。从外往里读:
repeat(times=3)是一次函数调用。它返回decorator。decorator才是真正的装饰器:它接收一个函数,返回一个包裹后的函数。wrapper是在调用时运行的包裹函数。
@retry(times=5)、@cache(maxsize=100) 以及 @app.route("/users") 这样的框架装饰器,用的都是这种结构。一旦你看懂了这种三层结构,这一整类装饰器读起来都一样。
叠加装饰器
你可以给一个函数应用多个装饰器。它们是“从下往上”叠加的:离 def 最近的那个最先执行:
add_exclaim 先包裹,加上 !。然后 shout 再包裹它,把所有内容转成大写。输出是 HI, ROSA!。
顺序很重要。把顺序反过来,你得到的仍是 HI, ROSA!,只是感叹号是在转成大写“之后”才加上的。在这里看起来一模一样,但想象一下一个格式化 JSON 的装饰器:让它在一个记录输入的装饰器之前还是之后运行,结果可能大不相同。
你会见到的内置装饰器
Python 及其标准库自带了几个装饰器,你在实际代码中会遇到它们:
@property把一个方法变成计算出来的属性。@staticmethod标记一个不使用self或cls的方法。@classmethod接收的是类(cls),而不是实例,非常适合用来写替代构造函数。@functools.lru_cache会缓存结果,用相同参数重复调用时直接命中缓存。
框架装饰器(@app.route、@pytest.fixture、@dataclass)用的也是同一套机制。没什么特别的,只不过是包裹函数的函数。
什么时候该写,什么时候不该写
当你想把同一种“行为”应用到许多函数上时,就写一个装饰器,比如计时、日志、重试、权限检查。关键在于让这种行为留在函数主体之外。
以下情况就别用装饰器了:
- 这种行为只属于某一个特定的函数。把它写进那个函数里。
- 你只是在测试时需要它。用测试夹具或参数会更清楚。
- 你忍不住想叠加四五个装饰器。到了这个地步,控制流就藏在装饰器链里了,读代码的人得一层层拆开,才能看清实际运行的是什么。一个直截了当的辅助函数可能更好读。
装饰器是一把锋利的工具。用得好,它能让代码不重复、意图清晰。用得不好,它会把程序在做什么藏起来。在决定要不要用装饰器时,倾向于选择“一目了然”的写法。
下一篇:类型提示
装饰器是在实际代码中经常遇到类型提示的地方,wrapper 函数常常会给自己的签名加上注解。类型提示是一个很快就能见效的小功能,下一篇就讲它。
常见问题
Python 中的装饰器是什么?
装饰器是一个函数,它接收另一个函数,返回一个新函数,通常是用额外的行为把原函数包裹起来的函数。使用方法是在 def 上面一行写 @decorator_name。@ 语法是 func = decorator_name(func) 的简写。
Python 中的装饰器有什么用?
在不修改函数主体的情况下,给函数加上前后的行为:日志、计时、缓存、身份验证检查、输入验证、重试。各种框架大量使用装饰器:Flask 中的 @app.route(...),pytest 中的 @pytest.fixture,还有内置的 @property 和 @staticmethod。
我可以自己写装饰器吗?
可以。装饰器只是一个接收函数、返回函数的函数。大多数自定义装饰器都会把原来的调用包进一个小的内部函数,在调用之前、之后或前后做点什么。在内部函数上使用 functools.wraps,可以保留原函数的名字和文档字符串。