前言
先澄清标题里一个容易误导的说法:「装饰器(decorator)」是 Python 的语言语法特性,从 PEP 318 开始进入语言,@something写在函数或类定义上方;而decorator是 PyPI 上一个真实的第三方库(由 Michele Simionato 维护,当前是 5.x 系列),它本身要靠pip install decorator安装,标准库里没有它。标题里把「库」和「装饰器语法」写在一起,读的时候要把这两件事分开。
至于「非常重要却又被严重低估」,这是主观评价,不必当真。这个库确实解决了一个真实痛点,但它解决的问题也有一部分已经被标准库的functools.wraps覆盖了。本文不吹它,只讲它到底补上了什么、什么时候值得装、什么时候标准库就够了。
还有一点要先说:本文所有关于decorator库的用法都只讲到思路层面,具体函数名、参数形式请以该库官方文档为准——库里 API 会随版本变化,本文不作为接口参考。
一、装饰器语法本身:它做了什么
@deco写在def f():上面的等价写法是:
# 适用于 Python 3.8+
def f():
...
f = deco(f)也就是:定义一个函数,立刻把它传给deco,用返回值替换掉原来的名字。所以装饰器必须返回一个可调用的东西。这条规则解释了两件事:装饰器不一定返回函数(可以返回类、返回None,虽然少见),以及装饰器在导入/定义时就执行,而不是调用f()时才执行。
要写一个通用装饰器,按惯例用*args, **kwargs透传:
# 适用于 Python 3.8+
def logged(func):
def wrapper(*args, **kwargs):
print("calling", func.__name__)
return func(*args, **kwargs)
return wrapper
@logged
def add(a, b):
return a + b
print(add(2, 3))calling add
5二、*args, **kwargs包装丢掉了什么
上面这个wrapper有两个问题:
- 元信息丢失。
add.__name__变成'wrapper',add.__doc__变成wrapper的文档(通常是None),打印日志、生成 API 文档、报错信息里全是wrapper。 - 签名丢失。
wrapper的真实签名是(*args, **kwargs),看不出add需要两个参数。
标准库给的解药是functools.wraps:
# 适用于 Python 3.8+
import functools
def logged(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("calling", func.__name__)
return func(*args, **kwargs)
return wrapperfunctools.wraps是functools.update_wrapper的便捷形式。按官方文档,它默认把WRAPPER_ASSIGNMENTS里的属性从原函数拷到包装函数上:__module__、__name__、__qualname__、__annotations__、__doc__,Python 3.12 起还默认拷贝__type_params__;同时用WRAPPER_UPDATES(即__dict__)做字典更新。更关键的是,它会给包装函数加上一个__wrapped__属性,指回原函数(这个行为从3.2开始自动生效,3.4起保证一定指向原函数)。
有了__wrapped__,inspect.signature(add)默认会顺着它找到原函数,因此在「内省」这个层面,签名其实已经被找回来了。所以不要再复述「不加functools.wraps就完全查不到签名」这种说法——准确的说法是:不加functools.wraps会丢掉__name__/__doc__等元信息,签名则要看具体工具是否遵守__wrapped__约定。
三、那decorator库补的是什么
decorator库的思路不是「拷贝属性」,而是生成一个真的拥有相同签名的包装函数。它的 README 里写得很直白:目标是把「保留签名的函数装饰器和装饰器工厂」变得容易写。用文档的话说,它让包装出来的函数在签名层面和原函数一致,而不是靠*args, **kwargs加属性拷贝去「装作」一致。
这样一来,在那些不按__wrapped__约定来内省的场合(某些框架自己解析函数签名、按参数名做依赖注入、把函数签名暴露给非 Python 工具的场景),包装后的函数仍然能正确报出参数。此外库的 README 还提到它附带实现了多重分派(multiple dispatch)等便利功能。
代价也摆在这里:
- 它是第三方依赖,标准库没有,要额外安装。
- 它生成包装函数时依赖运行时的代码生成/编译手法,行为比
functools.wraps复杂,出错时的堆栈也更绕。 - 因为要保签名,它比「
*args, **kwargs一把梭」的写法更严格,某些需要吞掉任意参数的装饰器反而不适合用它。
什么时候标准库就够了:绝大多数业务代码里的装饰器(打日志、计时、重试、权限校验、缓存),functools.wraps完全够用,而且没有额外依赖。什么时候才考虑它:你在写库或框架、需要把被包装函数的真实签名交给外部工具、且已经确认functools.wraps那条路在你的场景下不生效。装之前先验证问题是否真的存在,不要因为「听说这个库很厉害」就引入依赖。
四、自己实现签名保留的简化思路
如果只是想理解decorator库为什么需要做代码生成,可以看一个简化版的手写方案(仅用于说明原理,不追求通用):
# 适用于 Python 3.8+
import functools
import inspect
def logged(func):
sig = inspect.signature(func)
@functools.wraps(func)
def wrapper(*args, **kwargs):
# 用原函数的签名把参数绑定一次,既能校验数量,
# 也方便把关键参数打出来
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
print("calling", func.__name__, "with", dict(bound.arguments))
return func(*args, **kwargs)
return wrapper
@logged
def divide(a, b=1, *, precision=2):
return round(a / b, precision)
print(divide(10, 3, precision=3))calling divide with {'a': 10, 'b': 3, 'precision': 3}
3.333这里inspect.signature()拿到的是原函数的签名,Signature.bind会把实参按参数名整理成字典,参数个数不对时直接抛TypeError。这段代码说明了:要「真实」地保留签名行为,就得在运行时拿到签名并据此工作——decorator库做的事情更彻底,它是直接产出一个签名一致的函数对象。
常见坑点
- 装饰器不加
functools.wraps
❌def wrapper(*a, **k): ...直接 return,add.__name__变成'wrapper'✅ 在wrapper上加@functools.wraps(func)
- 把装饰器当成「调用时才执行」
❌ 以为@logged的打印会等到调用被装饰函数时才出现 ✅ 记住f = deco(f)在定义时执行,初始化逻辑不要写在这一层
- 忘记装饰器要
return包装函数
❌def deco(f): def w(): ...少了return w,被装饰的名字变成None✅ 确保每个分支都return一个可调用对象
- 带参数的装饰器少写一层
❌@retry(3)却把deco写成只接func一个参数,报TypeError✅ 带参数的装饰器要写成「返回装饰器的函数」,即三层嵌套
- 以为
functools.wraps能管住函数签名
❌ 断言「加了 wraps 后sig.parameters一定和别人手写的一致」 ✅ 它保的是元信息和__wrapped__;签名要靠工具遵守该约定才能还原
- 两个
@deco顺序搞反
❌ 以为靠近def的更晚执行 ✅ 装饰器自下而上套用,最靠近函数的那个最先被应用
- 给第三方库引依赖只为省几行代码
❌ 一上来就pip install decorator,增加了部署和版本冲突成本 ✅ 先确认functools.wraps不够用,再引入它,并以该库官方文档为准
- 在装饰器里保存被装饰函数的状态
❌ 把计数变量写在装饰器函数外层,多个函数共享同一个计数器 ✅ 把状态放在wrapper的闭包里或用functools.lru_cache这类现成机制
总结
| 方案 | 元信息 | 内省签名 | 依赖 | 适用场景 |
|---|
裸*args, **kwargs包装 | 丢失 | 看不到 | 无 | 临时脚本、不在乎内省 |
functools.wraps | 保留 | 靠__wrapped__还原 | 无(标准库) | 绝大多数业务装饰器 |
decorator库 | 保留 | 生成真签名一致 | 第三方,需安装 | 库/框架,或需要真签名的场合 |
先把functools.wraps用熟,这是成本和收益比最高的做法。decorator库值得了解的是它的问题定位——「包装函数的签名不一致」——以及它给出的解法;但要不要为它引入一个第三方依赖,取决于你的场景是否真的踩到了__wrapped__覆盖不到的地方。