☰
Python编程中非常重要却又被严重低估的库decorator
2026/10/13 7:20:35 网站建设 项目流程

前言


先澄清标题里一个容易误导的说法:「装饰器(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有两个问题:



  1. 元信息丢失。add.__name__变成'wrapper',add.__doc__变成wrapper的文档(通常是None),打印日志、生成 API 文档、报错信息里全是wrapper。

  2. 签名丢失。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 wrapper

functools.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库做的事情更彻底,它是直接产出一个签名一致的函数对象。


常见坑点



  1. 装饰器不加functools.wraps


❌def wrapper(*a, **k): ...直接 return,add.__name__变成'wrapper'✅ 在wrapper上加@functools.wraps(func)



  1. 把装饰器当成「调用时才执行」


❌ 以为@logged的打印会等到调用被装饰函数时才出现 ✅ 记住f = deco(f)在定义时执行,初始化逻辑不要写在这一层



  1. 忘记装饰器要return包装函数


❌def deco(f): def w(): ...少了return w,被装饰的名字变成None✅ 确保每个分支都return一个可调用对象



  1. 带参数的装饰器少写一层


❌@retry(3)却把deco写成只接func一个参数,报TypeError✅ 带参数的装饰器要写成「返回装饰器的函数」,即三层嵌套



  1. 以为functools.wraps能管住函数签名


❌ 断言「加了 wraps 后sig.parameters一定和别人手写的一致」 ✅ 它保的是元信息和__wrapped__;签名要靠工具遵守该约定才能还原



  1. 两个@deco顺序搞反


❌ 以为靠近def的更晚执行 ✅ 装饰器自下而上套用,最靠近函数的那个最先被应用



  1. 给第三方库引依赖只为省几行代码


❌ 一上来就pip install decorator,增加了部署和版本冲突成本 ✅ 先确认functools.wraps不够用,再引入它,并以该库官方文档为准



  1. 在装饰器里保存被装饰函数的状态


❌ 把计数变量写在装饰器函数外层,多个函数共享同一个计数器 ✅ 把状态放在wrapper的闭包里或用functools.lru_cache这类现成机制


总结




方案元信息内省签名依赖适用场景



裸*args, **kwargs包装丢失看不到无临时脚本、不在乎内省

functools.wraps保留靠__wrapped__还原无(标准库)绝大多数业务装饰器

decorator库保留生成真签名一致第三方,需安装库/框架,或需要真签名的场合



先把functools.wraps用熟,这是成本和收益比最高的做法。decorator库值得了解的是它的问题定位——「包装函数的签名不一致」——以及它给出的解法;但要不要为它引入一个第三方依赖,取决于你的场景是否真的踩到了__wrapped__覆盖不到的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询