Python 装饰器把函数"搞丢"了?5 个必踩的坑与完整避坑对照表

先给结论:装饰器最大的坑不是"不会写",而是"写完之后原函数被悄悄换掉了" ------名字、文档、签名、甚至它的身份都可能不是原来那个函数。functools.wraps 能救回大部分,但它救不了全部(坑 4 和坑 5 它就无能为力)。

下面 5 个坑,按"踩中频率 × 排查难度"排序,每个都给复现代码和修法。

坑 1:函数名和文档字符串凭空消失

症状 :用了装饰器之后,help() 看不到原函数的文档,logging 打出来的函数名全是 wrapper。

python 复制代码
def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f"调用 {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

@log_calls
def add(a, b):
    """返回两数之和"""
    return a + b

print(add.__name__)  # wrapper  ← 不是 add!
print(add.__doc__)   # None     ← 文档没了

原因 :@log_calls 等价于 add = log_calls(add)。变量 add 现在指向的是 wrapper 函数对象,原函数的元数据自然全丢了。

修法 :加 @functools.wraps。

python 复制代码
import functools

def log_calls(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f"调用 {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

print(add.__name__)  # add
print(add.__doc__)   # 返回两数之和

@wraps 会把原函数的 __name__、__doc__、__module__、__qualname__、__dict__ 复制到 wrapper 上,并设置 __wrapped__ 指向原函数。这是每个装饰器都该有的默认动作。

坑 2:以为 wraps 能让一切恢复原样(其实 signature 另说)

症状 :加了 wraps 后,inspect.signature 拿到的还是 (*args, **kwargs),参数校验/依赖注入框架认不出你的参数。

python 复制代码
import inspect, functools

def check(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@check
def mul(x: int, y: int = 2) -> int:
    return x * y

print(inspect.signature(mul))

这里 wraps 设置了 __wrapped__,而 inspect.signature 默认 follow_wrapped=True,会顺着 __wrapped__ 找到原函数,所以正常情况会打印 (x: int, y: int = 2) -> int。但如果你显式传了 follow_wrapped=False,或者装饰器是手写赋值而没设 __wrapped__,就会退化成 (*args, **kwargs)。

修法 :要么依赖 wraps 的 __wrapped__(推荐),要么在需要精确签名的地方显式 inspect.signature(func, follow_wrapped=False) 并自己处理。

坑 3:装饰器在导入时就执行了,不是调用时

症状 :你以为注册/计时逻辑要等函数被调用才跑,结果 import 完就已经跑了一遍;Flask 路由注册顺序诡异;进程启动时就打了一堆日志。

python 复制代码
REGISTRY = {}

def register(func):
    REGISTRY[func.__name__] = func   # import 时就执行
    return func

@register
def task(): pass

print(REGISTRY)  # 模块刚 import 完,这里已经有内容了

原因 :装饰器是模块加载阶段 执行的------@register 在 def 定义完成的那一刻就被调用了,和函数体是否被执行无关。

修法 :如果副作用必须延迟,把注册动作挪到 wrapper 内部(首次调用时执行),或者显式提供一个 init() 由调用方触发。记住这条判据:装饰器函数体的代码在 import 时跑,wrapper 函数体的代码在调用时跑。

坑 4:带参数的装饰器,少写一层

症状 :想写 @retry(times=3),结果报错或者行为完全不对。

python 复制代码
def retry(times):
    def wrapper(*args, **kwargs):
        ...
    return wrapper   # 返回的是 wrapper,但它接收的是原函数的位置参数

原因 :带参数的装饰器是三层结构:最外层接收装饰器参数,中间层接收被装饰函数,最内层才是真正的包装。

python 复制代码
import functools

def retry(times=3, exceptions=(Exception,)):
    def decorator(func):                  # ← 第二层:收函数
        @functools.wraps(func)
        def wrapper(*args, **kwargs):     # ← 第三层:收参数
            for i in range(times):
                try:
                    return func(*args, **kwargs)
                except exceptions:
                    if i == times - 1:
                        raise
        return wrapper
    return decorator                      # ← 第一层返回 decorator

@retry(times=3)
def fetch(url): ...

记忆口诀 :@retry 不带括号是两层,@retry(...) 带括号是三层。

坑 5:装饰器顺序,以及 lru_cache 的内存泄漏

症状 A :@staticmethod 和自定义装饰器叠在一起后,方法调用报参数个数不对。

python 复制代码
class A:
    @staticmethod
    @log_calls
    def f(x): return x

A.f(1)      # 正常

class B:
    @log_calls
    @staticmethod
    def f(x): return x

B.f(1)      # 可能抛 TypeError:包装器吃掉/改变了绑定行为

修法 :@staticmethod / @classmethod 永远放在最外层(最上面),让描述符协议最后生效。

症状 B :functools.lru_cache 装饰实例方法后,实例永远不被回收。

python 复制代码
class Service:
    @functools.lru_cache(maxsize=None)
    def load(self, key): ...

原因 :缓存的 key 里包含 self,缓存表强引用着所有调用过的实例,等于给每个实例钉了根钉子。另外 lru_cache 要求参数可哈希 ,传 list/dict 会直接 TypeError: unhashable type。

修法 :实例方法别用 lru_cache(改用 functools.cached_property,或把缓存挂在实例上并手动管理生命周期);参数不可哈希时先转成 tuple/frozenset。

一张对照表收尾

现象 根因 修法
__name__ 变成 wrapper、文档丢失 装饰器返回了新函数对象 @functools.wraps(func)
框架读不到原参数签名 未设 __wrapped__,或 follow_wrapped=False 依赖 wraps;或显式传 follow_wrapped
import 完副作用就跑了 装饰器体在加载时执行 副作用挪进 wrapper,或显式 init()
@retry(times=3) 行为错乱 少写一层(应为三层) 工厂 → decorator → wrapper
方法调用参数个数错 staticmethod 被包在里面 staticmethod 放最外层
实例不释放 lru_cache key 持有 self 实例方法不用 lru_cache
unhashable type 参数不可哈希 转 tuple/frozenset

最后

"wraps 解决元数据,解决不了语义" ------它能把名字和文档补回来,但补不回执行时机、参数绑定和内存生命周期。写装饰器时按这三问自查:① 加了 wraps 吗?② 副作用该在 import 时还是调用时?③ 有没有和描述符/缓存叠用?

你踩过最离谱的装饰器坑是哪个?评论区聊聊。

相关推荐
for_ever_love__1 小时前
容器类型全家桶——list、tuple、dict、set 与推导式
python·容器·数据类型·大模型开发
外收内放1 小时前
Python基础语法练习题(43-44)
开发语言·python
10年前端老司机1 小时前
LangChain 五种工具定义方式踩坑总结
python·langchain·llm
坤坤子吖1 小时前
Python基础语法学习:列表和元组
开发语言·笔记·python·学习
zwd20051 小时前
Manim arrange 和 arrange_in_grid 用法详解:buff、aligned_edge、rows/cols(0.21.0 实测)
前端·python·edge
外收内放2 小时前
Python基础语法练习题(拓展2)
python·pygame
“AI国潮设计-小江”2 小时前
《Python+SDXL实战:用ControlNet批量生成“英歌舞草莓蛋糕”IP,附自动化脚本与商用思路》
开发语言·人工智能·python·prompt·aigc
为啥全要学2 小时前
大模型流式输出过滤敏感词
开发语言·python
2601_962885722 小时前
如何用 Python 统计 A 股连续涨停(几连板)?
java·linux·python