Python装饰器必备:functools.wraps原理与实战

发布时间:2026/10/10 4:31:33
Python装饰器必备:functools.wraps原理与实战
1. 从一次排查事故说起先讲个我自己的故事。几年前在某项目组做接口层改造用的 Python。某天线上报了一个 bug定位到某个视图函数里抛了异常但日志里记录的报错堆栈第一行是in wrapper而不是函数名。我下意识用函数名去 grep 代码结果搜不到因为代码里写的函数叫get_user_profile而运行时的名称已经被替换成了wrapper。排查了三四个小时最终发现是团队里有人写装饰器时没有保留原函数的元信息所有被装饰过的接口全变成了同名的wrapper那段时间线上排障效率极低。这个问题的正解就是functools.wraps。wraps()是 Python 标准库functools模块中的一个装饰器工具核心作用一句话总结当你在写自定义装饰器时它能把被装饰函数的名称、文档字符串、注解、模块等元信息“复制”到包装函数上同时把原始函数挂到__wrapped__属性上。这样装饰器包装后的函数在使用体验上“看起来像”原函数调试、序列化、文档生成、IDE 提示都不会出幺蛾子。这篇博文写给谁两类人。一类是刚学装饰器、被各种嵌套搞晕的初学者另一类是已经在用装饰器但没深究过wraps内部机制、遇到脏元信息问题的进阶开发者。我会从装饰器本身的坑讲起拆解wraps的原理、源码级细节、真实项目中的正确姿势以及我踩过的一些坑。2. 装饰器为什么需要 wraps2.1 装饰器的本质是“替换”很多人第一次学装饰器时听到的一句话是“装饰器就是给函数增加功能”但这是结果描述不是机制描述。机制上装饰器做的事情是你写了一个函数比如my_decorator它接收一个函数作为参数返回一个新的函数然后 Python 在定义阶段完成一次“名称重新绑定”。def my_decorator(func): def wrapper(*args, **kwargs): print(before) result func(*args, **kwargs) print(after) return result return wrapper my_decorator def hello(): 返回问候语 return hello # 上面完全等价于 # def hello(): # return hello # hello my_decorator(hello)关键点就在最后一行hello这个名字不再指向原来你编写的函数对象而是指向了my_decorator内部创建的wrapper。原来的函数对象还在内存里但丢了名字。这会产生一连串连锁反应。2.2 丢了什么——五个名字和一堆元数据被替代后最直观的变化是函数“自我介绍”时的信息变了print(hello.__name__) # wrapper print(hello.__doc__) # None我把封装前后的属性对比拉个表这样更直观。每种属性对应一个丢失场景属性/行为包装前原函数包装后wrapper实际影响__name__hellowrapper日志、报错堆栈、序列化时函数名错乱__doc__返回问候语None帮助文档、IDE 悬停提示失效__annotations__参数和返回值注解{}类型检查工具失效__module__函数所在模块名当前模块名部分场景下定位困难__qualname__带路径的限定名称被覆盖调试器可读性下降__dict__原函数上挂的扩展属性通常是空框架扩展点失效签名signature(name, age18)(*args, **kwargs)IDE 提示、接口自省错误前四个属性是硬伤__name__变成wrapper是最普遍的现象几乎只要写一个装饰器就会遇到。报错堆栈里满屏的wrapper会让人瞬间失去排查欲望。2.3 丢失后的三大麻烦场景丢失元信息不是“丑”那么简单是实实在在的坑。我挑三个高频场景说。第一个是日志和监控。线上系统打日志时通常会把func.__name__记录进去用于追踪“哪个函数慢”“哪个函数在报错”。如果装饰器没有保留名字所有被装饰函数在日志里叫同一个名字性能瓶颈定位无从谈起异常聚合告警也会把不同函数合并成一条。第二个是 IDE、文档生成器和自动测试。现代 IDE 靠函数签名做自动补全、参数提示Sphinx 这类工具靠__doc__生成 API 文档某些测试框架靠__name__来动态发现用例。一旦元信息被覆盖程序员的一天就变成“看源码找参数”的一天。第三个是序列化和缓存。有些框架要用函数对象作为缓存键比如把func.__module__ func.__qualname__拼成 key。没有正确元信息时缓存 key 会碰撞导致返回了别的函数的缓存结果这是最难排查的一类 bug。正是因为这些问题Python 官方提供了functools.wraps让你用一行代码、一个装饰器拯救全部信息。2.4 顺带理解为什么不是“手动赋值”在没有wraps之前很多人也手动赋值最常见的写法是在wrapper里逐个改def my_decorator(func): def wrapper(*args, **kwargs): ... wrapper.__name__ func.__name__ wrapper.__doc__ func.__doc__ wrapper.__module__ func.__module__ wrapper.__qualname__ func.__qualname__ wrapper.__annotations__ func.__annotations__ return wrapper这种写法有两个问题。第一个问题容易漏。Python 的属性很多今天写全了明天改需求又漏一个而且从func上取属性时一旦属性不存在还会抛AttributeError处理起来很啰嗦。第二个问题__dict__没同步。假设原函数上挂了func.custom_meta v1那wrapper上并没有这个属性而 Python 的__dict__是函数挂载扩展属性的地方不合并__dict__等于“函数本身上的自定义属性全部丢光”。3. wraps 的原理和源码细节3.1 它不是魔法是 copy 加贴标签wraps其实只是一个函数工厂定义在functools模块里。它的完整定义不同 Python 版本略有差异但核心一致拆解下来就三件事WRAPPER_ASSIGNMENTS (__module__, __name__, __qualname__, __annotations__, __doc__) WRAPPER_UPDATES (__dict__,) def update_wrapper(wrapper, wrapped, assignedWRAPPER_ASSIGNMENTS, updatedWRAPPER_UPDATES): for attr in assigned: try: value getattr(wrapped, attr) except AttributeError: pass else: setattr(wrapper, attr, value) for attr in updated: getattr(wrapper, attr).update(getattr(wrapped, attr, {})) wrapper.__wrapped__ wrapped return wrapper def wraps(wrapped, assignedWRAPPER_ASSIGNMENTS, updatedWRAPPER_UPDATES): def decorator(wrapper): return update_wrapper(wrapper, wrapped, assignedassigned, updatedupdated) return decorator逐行解读。第一件事从原函数wrapped上读取五个属性的值赋给wrapper。读取时用了try/except AttributeError防御了“原函数没有某个属性”的情况不会因为缺属性导致崩溃。第二件事把原函数__dict__里的所有键值对合并进wrapper.__dict__。注意这里是update不是覆盖整个字典所以wrapper已有的属性不会被冲掉原函数的自定义属性会加进来。第三件事标记wrapper.__wrapped__ wrapped。这一行极其关键它相当于在包装上留了一条“通往原始函数”的线索。后面讲inspect.unwrap时会再用到它。3.2 两个常量为什么这么设计WRAPPER_ASSIGNMENTS是一个元组里面是字符串属性名。这个设计意图是“只复制那些描述函数身份的信息”不复制函数体、不复制默认参数、不复制__class__。也就是说wraps做的事是“贴标签”而不是“克隆函数”。这符合装饰器的本意你还是一个全新的函数只是让外部看起来和原来一样。WRAPPER_UPDATES里只有一个__dict__。原函数上挂的额外属性会被合并到包装函数上。这里有个细节getattr(wrapper, attr).update(...)要求wrapper本身有一个__dict__而函数对象天然都有所以不会报错但如果你包装的是一个特殊对象比如某些实现了__call__的类实例但实例没有__dict__就得注意这种情况我会在后面的“坑”里专门讲。3.3 wraps 和 update_wrapper 的关系很多人第一次看到functools.update_wrapper时容易混淆。我拆开说清楚update_wrapper是执行复制动作的函数它接收wrapper、wrapped两个函数对象直接改属性。wraps是基于update_wrapper的装饰器工厂它接收原函数返回一个装饰器。所以实际场景中如果你在写一个需要“手动”更新元信息的工具函数可以直接调update_wrapper如果你在写装饰器用wraps更标准。我还会在写类装饰器时手动调用update_wrapper因为类装饰器的包装对象不是函数直接加wraps时附着的目标变了语义没那么直观。后面会有对应示例。3.4 一个比喻快递包裹和面单把wraps理解成“快递包装”特别好使。原函数是你真正要寄的东西装饰器是快递员把东西塞进包装盒并封口。问题是如果包装盒上不写姓名地址元信息收件人拿到一个完全认不出内容的盒子就只能拆开看里面有什么。wraps做的事就是把原包装原函数上的“面单”完整撕下来贴到新包装上。盒子里装的东西变了但面单说它是什么它就是什么。__wrapped__属性则是“快递单号可溯源”——你可以通过单号查到原始寄件人拿到函数本体。这就是inspect.unwrap的工作机制。4. 从入门到实战wraps 的正确使用姿势4.1 朴素装饰器一行 wraps 解决最基础的使用方式是在wrapper上面一行加wraps(func)import functools import time def timer(func): functools.wraps(func) def wrapper(*args, **kwargs): start time.perf_counter() try: result func(*args, **kwargs) finally: elapsed time.perf_counter() - start print(f{func.__name__} 耗时 {elapsed:.4f}s) return result return wrapper timer def compute(a, b): 两数之和 return a b print(compute.__name__) # compute print(compute.__doc__) # 两数之和 print(compute(1, 2)) # 3同时打印耗时注意写法wraps(func)里传的是被装饰的那个函数func不是你自己写的wrapper。这一行写在wrapper定义之上因为装饰器语法会把wrapper作为参数传给wraps(func)返回的装饰器。理解这一步后面带参数的装饰器才会顺。4.2 带参数的装饰器wraps 放在最内层写一个带参数的装饰器时函数嵌套层数会变成三层外层接收参数中间层接收函数内层才是真正替代原函数的wrapper。wraps必须放在最内层的wrapper上def retry(max_attempts3, delay1.0): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_attempts 1): try: return func(*args, **kwargs) except Exception: if attempt max_attempts: raise time.sleep(delay) return wrapper return decorator retry(max_attempts5, delay2.0) def fetch_data(url): 拉取远程数据 ...判断标准很简单wraps的语法糖只能修饰紧跟其后的那个函数定义。只要你看代码时发现“这个函数是真正执行原函数调用的”那么它就是wrapperwraps(func)就挂在它头上。这个规则百试百灵。4.3 类装饰器和call对象注意 update_wrapper 的目标类装饰器和类实现__call__是两类场景它们的包装对象不是普通函数。第一种类装饰器。你有一个类想用一个函数包住它的构造调用def singleton(cls): functools.wraps(cls) def wrapper(*args, **kwargs): if not hasattr(cls, _instance): cls._instance cls(*args, **kwargs) return cls._instance return wrapper singleton class Config: 应用配置这种场景下functools.wraps(cls)把类属性里能复制的__module__、__name__、__qualname__、__doc__复制给了wrapper函数。类对象有__name__和__doc__所以wrapper.__name__会变成Config文档字符串也会保留。调用Config()时实际上调用的是wrapper但它“自称”叫Config排障时不会一头雾水。第二种类实例实现__call__做成可调用对象。这是很多框架里“装饰器类”的实现方式class cached: def __init__(self, func): self.func func self.cache {} functools.update_wrapper(self, func) def __call__(self, *args, **kwargs): key (args, tuple(sorted(kwargs.items()))) if key not in self.cache: self.cache[key] self.func(*args, **kwargs) return self.cache[key] cached def slow_add(a, b): 慢速加法 import time time.sleep(1) return a b print(slow_add.__name__) # slow_add这里用了update_wrapper而不是wraps。原因是wraps的默认WRAPPER_UPDATES (__dict__,)会执行getattr(wrapper, attr).update(...)但实例的属性存储是self.__dict__不是函数的__dict__。如果对实例直接调用wraps它可能会尝试访问一个不存在的函数式__dict__行为不一致。手动用update_wrapper(self, func)并把updated设为空元组更安全functools.update_wrapper(self, func, updated())这样“复制名字、文档、模块等标识信息”但不同步__dict__因为实例自身的状态我们也想保留。4.4 解开包装inspect.unwrap 和wrapped的回溯wraps在wrapper上设置了__wrapped__ func这给了我们一个反向操作手段。当需要从包装后的函数拿到原函数时用inspect.unwrapimport inspect def a(func): functools.wraps(func) def inner(*args, **kwargs): return func(*args, **kwargs) return inner def b(func): functools.wraps(func) def inner(*args, **kwargs): return func(*args, **kwargs) return inner # 多层装饰叠加 a b def original(): 原始函数 ... # 拿到最里面的函数 print(original.__name__) # original因为每层都用了 wraps print(original.__wrapped__.__name__) # original unwrapped inspect.unwrap(original) print(unwrapped is original) # Falseinspect.unwrap会沿着__wrapped__链一层层剥直到某个函数没有该属性。这个机制在调试和框架底层很实用。比如某些测试框架用inspect.unwrap找到真正被装饰的函数然后读取其参数注解来推导测试用例。顺带提一句多层装饰时如果不加wrapsinspect.unwrap在第一层就断了所以“每个装饰器都写 wraps”是保证整条链路整洁的前提。4.5 自定义要复制的属性和更新方式wraps函数签名里还有两个可选参数assigned和updated。默认值是前面说的两个元组。如果需要额外复制别的属性可以传自定义元组def my_decorator(func): functools.wraps(func, assignedfunctools.WRAPPER_ASSIGNMENTS (my_flag,)) def wrapper(*args, **kwargs): ... return wrapper这个特性用到的场景不多但确实存在。在我参与的一个项目里团队约定所有接口函数必须挂一个api_version属性用于接口兼容性识别。装饰器里光靠wraps默认行为拿不到这个属性就得显式加进assigned元组。另一个思路是不要刻意去复制自定义属性而是依赖__dict__的update机制。因为WRAPPER_UPDATES里的__dict__更新会把原函数上所有自定义属性同步过来。如果你自定义的属性刚好在__dict__里通常都在就不用改assigned。只有原函数的属性是通过__slots__或者描述符实现时才需要特殊处理。5. 核心实战应用场景代码逐行分析5.1 场景一权限校验装饰器的工程化写法权限校验是装饰器应用最多的场景之一。大多数项目里会写成“需要登录”“需要角色”“需要权限”三层每一层都要wraps。我写一个工程化示例import functools from typing import Callable, Any def require_permission(permission: str): def decorator(func: Callable) - Callable: functools.wraps(func) def wrapper(*args, **kwargs) - Any: user getattr(args[0], current_user, None) if user is None: raise PermissionError(未登录) if not user.has_perm(permission): raise PermissionError(f缺少权限: {permission}) return func(*args, **kwargs) return wrapper return decorator几个工程细节值得说。getattr(args[0], current_user, None)是为了兼容“绑定方法被装饰”的情形。绑定方法第一个参数是self从self上取用户信息是常规做法。如果装饰器也用于普通函数第一个参数没有current_user会静默拿到None同样会触发未登录异常逻辑合理。functools.wraps(func)保证了这个装饰器包装后的方法在 IDE 里提示签名时显示的是函数本身的参数名而不是*args: Any, **kwargs: Any。我见过一个项目没用wraps结果所有接口的自动生成文档里参数列表全部变成了*args、**kwargs前端同学对接口调参全靠猜。5.2 场景二日志装饰器别让日志失去函数名日志装饰器看似简单但最容易忽略wraps带来的排障收益def trace_logger(logger): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): logger.info(f调用 {func.__name__}, args{args}, kwargs{kwargs}) try: result func(*args, **kwargs) except Exception as exc: logger.exception(f{func.__name__} 执行异常: {exc}) raise else: logger.debug(f{func.__name__} 返回 {result}) return result return wrapper return decorator注意wrapper内部的日志行用的是func.__name__而不是wrapper.__name__。虽然有了wraps后两者相等但func.__name__更保险因为它在wraps赋值之前就已存在。如果某一天你把wraps误删了wrapper.__name__会变成wrapper日志里全是垃圾信息而func.__name__依然正确。这是一个值得养成的编码习惯在装饰器内部读函数信息优先读参数绑定的func不要依赖复制后的wrapper。5.3 场景三缓存装饰器与签名冲突缓存装饰器容易和wraps碰撞出的问题是缓存的 key 设计。如果你用inspect.signature来生成 key那wraps因为不更新函数签名所以inspect.signature(fib)显示的是(*args, **kwargs)而不是(n)这会让 key 生成逻辑出错——你以为拿到了n结果拿到一个空args或全空的绑定。这就引出下一节的核心坑wraps到底改没改签名6. 五个高频坑和排查清单6.1 坑一wraps 不改变函数签名该不该算坑这是最多人误解的地方。用inspect.signature去检查使用了wraps的装饰器函数会得到timer def compute(a, b10): ... print(inspect.signature(compute)) # (*args, **kwargs)原因在于wraps复制的是__wrapped__之前列出的那几个属性不包括__signature__而且inspect.signature默认只看最终函数对象的__code__和__annotations__不会自动跑到__wrapped__去。也就是说wraps做了“身份伪装”但没有做“签名伪装”。解决方案很直接给wrapper设置__signature__属性。标准写法def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) wrapper.__signature__ inspect.signature(func) return wrapperinspect.signature会优先读取__signature__这样外部工具就能拿到正确签名。在写供别人调用的装饰器库时这一步建议加上。我踩过一次坑写了一个带缓存的装饰器内部用签名做缓存 key签名显示成(*args, **kwargs)导致所有调用都命中同一个缓存调试了很久才发现是签名问题。6.2 坑二多个装饰器叠加时 wraps 的复制顺序多个装饰器叠加时装饰器的执行顺序是从下往上。这影响一个细节如果最里层装饰器没有wraps外层装饰器的wraps(func)拿到的是什么outer inner def f(): ...inner装饰后返回的是它的inner_wrapperouter拿到的是inner_wrapperfunctools.wraps(inner_wrapper)复制的属性来自inner_wrapper而不是原始f。如果inner_wrapper自己没有正确处理元信息这些信息就可能是一层污染过的东西。所以最佳实践是每个装饰器都要在各自的wrapper上加wraps。这等于“接力赛每一棒都规范交接”最后一棒拿到的信息才是完整的。6.3 坑三类装饰器中 update_wrapper 和dict的兼容问题前面提过wraps默认会同步__dict__。对一个类实例对象执行update_wrapper(self, func)时如果updated(__dict__,)会调用self.__dict__.update(func.__dict__)。这通常没问题但有一个隐患实例上已有的状态属性会被原函数上的同名扩展属性覆盖。比如原函数挂了个func.cache_size 10而实例自己也用self.cache_size 20做缓存控制那么update_wrapper会覆盖它。解决方法是把updated替换成空元组或者用一个独立属性名保存这些扩展属性。我的判断标准是实例对象不参与扩展属性的同步类装饰器的重点只放在复制__name__、__doc__这些标识信息上。6.4 坑四装饰器包装后序列化 pickle 失败被装饰的函数如果直接pickle.dumpsPython 的序列化机制会按模块路径和函数名去查找找不到wrapper就会报错。加上wraps后__module__和__qualname__被复制pickle 就能按原函数的路径找到代码但序列化的仍然不是你想象的那个对象。这个问题不存在完整解因为wrapper本身是一个新的代码对象在模块里没有与之对应的顶层名字。实际项目里凡是需要做进程间传递回调函数的场景我都不建议对一个被装饰函数直接pickle应该用inspect.unwrap取出原函数再序列化否则大概率翻车。6.5 坑五partial 与 wraps 混用时的属性来源functools.partial对象也常与包装场景混在一起。partial 包装后的对象也有func属性但没有__name__、__doc__。如果你写一个装饰器对 partial 对象执行functools.wraps(partial_obj)赋值时会因缺少属性而跳过不会报错但__name__会保持原样也就是空。要处理这个场景可以抽取partial_obj.func上的属性或者避免对 partial 直接做 wraps。一个常用的做法def my_decorator(func): if isinstance(func, functools.partial): wrapped func.func else: wrapped func functools.wraps(wrapped) def wrapper(*args, **kwargs): ...6.6 排查清单装饰器干了什么写一个装饰器并怀疑元信息有问题时我会按顺序检查四个点func.__name__是不是原函数的名称。inspect.signature(func)是否保留了参数名。inspect.unwrap(func)是否能找到原函数。用help(func)看文档字符串是否正常。其中任何一项失败都说明装饰器的元信息处理不合格。实际项目中我把它做成一个单元测试模板每个装饰器写一条用例断言__name__、__doc__、__wrapped__三个属性。自动回归防止有人不小心删掉wraps。7. 标准库与主流框架中的 wraps 身影wraps不是小工具的命它是 Python 标准库很多装饰器的地基。比如functools.lru_cache源码里就用了update_wrapper来保证被缓存函数保留原名和文档。再比如contextlib.contextmanager、asyncio的某些运行控制装饰器、unittest.mock里的 mock 对象包装底层全部依赖这同一个机制。框架层面我实际接触过的有某个 Web 框架的鉴权装饰器、某个 RPC 框架的注册装饰器、某个配置系统的属性装饰器。几乎每个 Python 框架源码里的装饰器都能看到functools.wraps或者基于它的封装。这也侧面说明装饰器如果不搭配wraps在工程上很难活得久。举一个非常直观的标准库例子lru_cache的使用者们一定见过这个现象。缓存函数的__name__、__doc__都保持原样等于它内部自动做了元信息保留。你不需要给lru_cache再套一层wraps因为它自己的实现已经做了这件事。标准库的规范对你的自定义装饰器有示范意义。8. 关于 wraps 的几个冷知识8.1 为什么官方把几个属性做成“元组”而不是“集合”WRAPPER_ASSIGNMENTS用的是元组因为属性复制需要顺序和确定性。字符串、可迭代对象的顺序在 Python 里通常有保证但元组写死了顺序阅读源码时不会出现“字典遍历顺序导致赋值顺序不同”的疑问。虽然属性赋值顺序日常无感但这些细节体现了标准库代码的严谨。8.2 浅拷贝和内存开销wraps的所有操作都是引用赋值不是深拷贝。wrapper.__doc__和原函数共享同一个文档字符串对象wrapper.__annotations__直接引用同一个字典。因此wraps几乎不产生额外内存开销。但要注意如果你没有用wraps而是自己写了个复制函数误用了copy.deepcopy复制注解字典反而会引入不可见的内存膨胀。所以我一直推荐优先用标准库方案不要手搓。8.3 函数签名自省工具 inspect.signature 的处理差异经常有人问既然inspect.signature在包装后函数上看到的是(*args, **kwargs)那如果手动设置了__signature__inspect.signature会优先读它这是不是就可以说wraps能改签名严格讲wraps本身不改签名签名显示正确是因为你额外设了__signature__。如果再往上一层inspect.signature还会看__wrapped__吗不会。它会看__signature__然后是__annotations__然后是__code__不沿__wrapped__链递归。这和inspect.unwrap的逻辑不同很多人在这里踩坑。8.4 Python 3.3 之前没有 wraps 吗functools.wraps从 Python 2.5 就已经存在历史非常悠久。后来在 Python 3.2 中加入的__wrapped__属性则是一次重要增强它让“解包”成为可能。这意味着很多老代码里可能没有__wrapped__需要检查运行时行为时先用hasattr判断。新代码则没有这个顾虑。9. 我个人的一套装饰器“标准草案”我在写装饰器时基本遵循一套默认模板可以分享出来当参考import functools import inspect def decorator_template(funcNone, *, optionNone): if func is None: return lambda f: decorator_template(f, optionoption) functools.wraps(func) def wrapper(*args, **kwargs): # 在这里写前置逻辑 result func(*args, **kwargs) # 在这里写后置逻辑 return result wrapper.__signature__ inspect.signature(func) return wrapper几个设计决策既支持decorator_template也支持decorator_template(option...)判断func is None即可区分两种调用法。保留签名因为现代 IDE 和文档工具对签名提示依赖度很高。用func变量名直读原函数避免内部逻辑依赖wrapper.__name__。装饰器内部绝不修改func本身所有附加行为都在wrapper里完成。这套模板在我经手的项目里被直接复制过它解决的不仅是元信息问题也统一了团队写装饰器的风格。10. 最后分享两个实操体会第一点装饰器内读取函数元信息时不要把wrapper当成信息来源。写完functools.wraps(func)后wrapper.__name__ func.__name__是成立的但代码可读性和鲁棒性上直接使用闭包绑定的func更干净。我见过有人这么写functools.wraps(func) def wrapper(*args, **kwargs): logger.info(f调用 {wrapper.__name__})这个代码能跑但一旦将来有人把wraps删掉调试日志就错了。改成func.__name__后逻辑和wraps解耦更抗折腾。第二点写库给别人用的时候wraps只是及格线。真正专业级别的装饰器还需要处理好__signature__、自定义属性、类装饰器兼容性并在文档里明确说明“被装饰函数的行为变化”。对一个面向团队或社区发布的装饰器组件这部分投入非常值得。实际项目里我在“替换装饰器”时有一个土办法给装饰器写一条专门的测试用例断言原函数对象和包装后对象的__name__、__doc__、inspect.unwrap行为后面所有重构都受益于这条用例。装饰器是 Python 里最能体现“小而美”的语法糖而wraps就是保证它甜而不腻的关键。