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

简介: 装饰器的坑不是"不会写",而是写完原函数被悄悄换掉——名字、文档、签名、身份都可能变。functools.wraps 救得回大部分,救不了全部。5 个坑逐个给复现代码和修法,附完整对照表。

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

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

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

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

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

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。

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),参数校验/依赖注入框架认不出你的参数。

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 路由注册顺序诡异;进程启动时就打了一堆日志。

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),结果报错或者行为完全不对。

# 错误:只写了两层
def retry(times):
    def wrapper(*args, **kwargs):
        ...
    return wrapper   # 返回的是 wrapper,但它接收的是原函数的位置参数

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

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 和自定义装饰器叠在一起后,方法调用报参数个数不对。

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 装饰实例方法后,实例永远不被回收。

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 时还是调用时?③ 有没有和描述符/缓存叠用?

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

相关文章
|
15天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
8131 15
|
13天前
|
人工智能 并行计算 PyTorch
秋叶 ComfyUI 2026 整合包 v3.2 完整部署教程:Python 3.13 + Torch 2.13 全栈升级
秋叶aaaki ComfyUI 2026年8月整合包v3.2正式发布!全面升级Python 3.13.11、PyTorch 2.13.0+cu130及ComfyUI v0.30.2,原生支持MiniMax H3、Wan 2.2、Qwen-Image-2.1等2026主流音视频/图像模型,解压即用,无需环境配置。
2226 13
|
13天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
1825 4
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
12天前
|
人工智能 编解码 并行计算
MiniMax-H3 一键整合包技术文档:8G 显存运行 AI 漫剧制作 —— 角色替换 / 动作迁移 / 文图生视频部署与调参指南
MiniMax H3 是 MiniMax 开源的全模态视频生成模型,支持文/图/音/视多条件输入,输出最高2K、15秒带双声道音频视频。本文档详述其Int8量化版在8GB显存下的本地一键部署、三段式工作流(EDIT/REPLACE/CONTINUE)、参数调优及常见问题排查。(239字)
|
7天前
|
人工智能 Linux 开发者
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
Codex是OpenAI推出的AI编程智能体,可读取本地项目、理解需求并自动修改代码。支持桌面GUI、命令行(CLI)及VS Code/Cursor插件三种形态,覆盖可视化操作、终端高效开发与编辑器无缝集成场景,助开发者用自然语言驱动编码全流程。(239字)
【2026国内使用】Codex安装过程一篇讲透(Win/Mac/Linux全支持)
|
7天前
|
人工智能 JSON 编解码
【2026最新版】ComfyUI本地部署教程,新手也能看懂!
ComfyUI是本地运行的AI绘画工具,采用节点式工作流设计:通过拖拽连接“加载模型”“提示词编码”“采样”“解码”等模块,实现高度可控的文生图。新手推荐使用秋叶整合包,一键启动、内置模型管理与插件安装器,轻松上手。(239字)
|
21天前
|
缓存 IDE Java
【保姆级】Android Studio下载、安装和汉化教程(2026最新)
Android Studio 是 Google 官方推出的免费 Android 应用开发集成环境,基于 IntelliJ IDEA,内置模拟器、调试器、性能分析及 Compose 界面工具,功能全面,文档丰富,是安卓开发首选工具。(239字)
2277 1

热门文章

最新文章