多个装饰器叠加时,定义阶段的包装顺序为func = A(B(C(func))),即离函数最近的装饰器C最先包装,B次之,A最后;调用时执行顺序呈“洋葱模型”:A前置→B前置→C前置→原函数→C后置→B后置→A后置。

装饰器叠加时的定义阶段:@A @B @C func() 等价于 func = A(B(C(func)))
这不是“执行顺序”,而是**包装顺序**——Python 解释器在模块加载时就完成了这一步。它从离 func 最近的装饰器开始,一层层往上套:C 先处理原始函数,B 接收 C 的返回值(即被 C 包装后的函数),A 再接收 B 的返回值。这个过程发生在函数被调用之前,甚至可能早于你 import 模块的那一刻。
常见错误现象:@log @cache def f(): ... 却发现日志里没记录缓存命中——很可能因为 @log 在外层,它看到的是“是否命中缓存”这个动作,而不是“是否真正执行了原函数”。
- 如果想让日志记录真实执行路径,
@log应该放在最内层(即离函数最近) - 如果想让日志包含缓存决策逻辑(比如“跳过执行,命中缓存”),
@log就得在外层 - 装饰器本身不能有副作用依赖“其他装饰器已生效”,因为它们的包装是静态的、不可逆的
调用时的实际执行流:像剥洋葱,从最外层 wrapper 进入,逐层向内,再原路返回
当你写 f(),实际触发的是 A 返回的 wrapper。它内部会执行自己的前置逻辑,然后调用 B 返回的 wrapper,后者再调用 C 的 wrapper,最后才到原函数。返回时则相反:原函数 return → C 后置逻辑 → B 后置逻辑 → A 后置逻辑。
使用场景举例:你想实现“先鉴权、再缓存、最后执行”,那么顺序必须是 @auth @cache @actual_logic——因为 @auth 是最外层,它最先拿到控制权;而 @actual_logic 是最内层,它只在前面所有检查都通过后才运行。
图片提示词生成器?不止如此。 马甲系统 —— 把脑海中的画面,翻译成AI能理解的专业表达。 用得越多,它越懂你:首次需要多问几句确认方向,用久了几乎一说就懂。 用得越多,它越快:缓存机制让后续对话越来越省。 RAG进化:成功案例持续入库,越跑越聪明。 输入「新手指南」查看完整功能介绍
立即学习“Python免费学习笔记(深入)”;
- 前置逻辑(before)按声明顺序从上到下执行
- 后置逻辑(after)按声明顺序从下到上执行
- 异常不会自动跨装饰器传播:如果
C抛异常,B和A的后置逻辑默认不执行,除非你在每个wrapper里显式加try/except/finally
带参数装饰器(如 @retry(max_attempts=3))会让嵌套更难读,但规则不变
带参装饰器本质是三层函数:retry 接收参数并返回一个真正的装饰器,后者再接收函数。所以 @retry(3) @log def f():... 展开后仍是 f = retry(3)(log(f))。关键点在于:括号里的调用(retry(3))发生在定义阶段,它必须返回一个可调用对象,否则你会遇到 TypeError: 'NoneType' object is not callable。
- 漏掉中间层的
return是高频错误,比如在decorator函数末尾忘了return wrapper -
functools.wraps(func)必须用在最内层wrapper上,否则f.__name__会变成wrapper,影响调试和序列化 - 多个带参装饰器叠加时,参数求值顺序和包装顺序一致:先算最靠近函数的那个装饰器的参数表达式
最容易被忽略的点:装饰器代码在模块导入时就执行,不是调用时
这意味着如果你在装饰器里写了 print("init") 或初始化数据库连接,它会在 Python 加载模块时立刻运行,而不是等你第一次调用函数。很多线上问题——比如启动时报错、连接池提前耗尽、配置未加载就初始化——都源于这个时间点误判。
复杂点在于:你无法靠“函数是否被调用”来判断装饰器是否生效;它生效与否,只取决于模块是否被 import 进来。哪怕那个函数永远不被调用,只要模块加载了,所有 @ 都已完成包装。

















