用functools.wraps可修复装饰器导致的help()和__name__失效问题,它自动复制__name__、__doc__、__annotations__等元数据;多层装饰器下可用inspect.unwrap()递归获取原始函数。

装饰器让help()和__name__失效,怎么办
不加处理的装饰器会覆盖原函数的元数据,导致help(func)显示装饰器内部的签名、func.__name__变成wrapper、func.__doc__丢失。这不是性能问题,但会让调试、IDE提示、文档生成全部出错,间接拖慢开发节奏。
根本原因是装饰器返回的是新函数,Python 默认不继承原函数属性。解决方式不是“避免用装饰器”,而是用functools.wraps显式复制元数据:
from functools import wraps
<p>def my_decorator(func):
@wraps(func) # ← 关键:把 func 的 <strong>name</strong>、<strong>doc</strong>、<strong>annotations</strong> 等全拷过去
def wrapper(*args, *<em>kwargs):
print("before")
result = func(</em>args, **kwargs)
print("after")
return result
return wrapper
-
@wraps(func)本质是调用update_wrapper(wrapper, func),复制__module__、__name__、__qualname__、__doc__、__annotations__等 - 如果只关心
__name__和__doc__,可以手动赋值:wrapper.__name__ = func.__name__,但漏掉__annotations__会导致类型检查工具(如mypy)报错 - 第三方装饰器库(如
decorator包)默认做元数据透传,但标准库@lru_cache、@property等已内置wraps,无需额外处理
装饰器嵌套后__wrapped__链断裂,怎么查原始函数
多层装饰器(比如@cache @validate @log)会让func.__wrapped__只指向紧邻的上一层,无法直接拿到最内层原始函数。这对单元测试打桩、动态分析或框架反射很麻烦。
Python 3.11+ 提供inspect.unwrap()递归解包,兼容所有标准@wraps装饰器:
立即学习“Python免费学习笔记(深入)”;
让 macOS 自动化像说话一样简单 —— 用代码控制你的 Mac,无需学习 AppleScript 系统要求 macOS 14+ (Sonoma) Python 3.9+ 需要开启:辅助功能 + 屏幕录制权限
import inspect <p>@cache @validate def risky_api_call(): return 42</p><p>original = inspect.unwrap(risky_api_call) # ← 返回真正的 risky_api_call 函数对象 assert original.<strong>name</strong> == "risky_api_call"
- 旧版本(inspect.unwrap(func, stop=lambda f: hasattr(f, '__wrapped__') is False)模拟,但不如原生可靠
- 手动遍历
__wrapped__链有风险:某些装饰器(如dataclasses.field相关)可能伪造该属性,unwrap()内部做了安全校验 - 若某个装饰器没用
@wraps,unwrap()会在那层停止,此时需联系该库作者修复,或改用functools.partial替代装饰器逻辑
用@wraps真能减少性能损耗吗
不能。@wraps本身只是元数据拷贝,发生在装饰器定义时(import 或模块加载阶段),不参与每次函数调用。它对运行时性能零影响——所谓“损耗”其实来自装饰器主体逻辑,比如日志序列化、缓存哈希计算、参数校验循环。
真正要优化的是装饰器内部,而不是纠结@wraps:
- 避免在
wrapper里做重操作:比如把json.dumps(args)改成仅在 debug 模式下执行 - 缓存装饰器自身构造开销:对带参数的装饰器(如
@retry(times=3)),把@wraps放在最外层闭包,而非每次调用都重复update_wrapper - 用
__slots__ = ()定义wrapper类(如果用类实现装饰器),减少实例字典开销;但函数式装饰器无需考虑这点 - 极端场景可考虑宏式展开(如
exec()拼接代码),但破坏可读性和调试性,得不偿失
为什么有些装饰器加了@wraps还是丢__signature__
functools.wraps默认不复制__signature__,因为签名可能被装饰器逻辑修改(例如@dataclass会注入self参数)。如果你的装饰器需要保持原签名(比如用于 FastAPI 路由或 Pydantic 验证),必须手动更新:
from functools import wraps from inspect import signature <p>def sig_preserving_decorator(func): @wraps(func) def wrapper(*args, *<em>kwargs): return func(</em>args, **kwargs)</p><h1>手动同步 signature,否则 IDE 和类型检查可能误判参数</h1><pre class="brush:php;toolbar:false;">wrapper.__signature__ = signature(func) return wrapper
-
__signature__不是普通属性,它是Signature对象,不能靠setattr简单复制,必须用inspect.signature()重建 - 如果装饰器修改了参数(如自动注入
request对象),应构造新的Signature,而不是硬塞原签名,否则类型检查会失败 - Pydantic v2 的
@validate_call、FastAPI 的@router.get都依赖准确的__signature__,漏掉这步会导致参数解析异常或 422 错误
元数据透传不是“锦上添花”,而是装饰器能被工程化使用的底线。很多人卡在<strong>name</strong>不对就放弃整个装饰器方案,其实就差一行@wraps——但反过来,加了@wraps也不代表签名、类型注解、断点调试就自动好使,每个环节都得单独验证。


















