比如下面这段代码,@decorator1和@decorator2的执行顺序是什么?调用func()时,两个装饰器的逻辑会以怎样的顺序触发?
def func():print("执行原函数")
今天就通过 “自定义装饰器 + 代码验证” 的方式,拆解多个装饰器的装饰阶段和调用阶段执行顺序,再总结规律与避坑点,让你彻底搞懂多装饰器的执行逻辑。
在分析多个装饰器前,必须先明确一个关键概念:装饰器的执行分为 “装饰阶段” 和 “调用阶段”,这两个阶段的执行顺序完全不同。
1.装饰阶段:Python 解释器加载代码时,会自动执行装饰器函数,对原函数进行 “包装”(此时原函数还没被调用);
2.调用阶段:当我们主动调用被装饰后的函数时,才会执行装饰器中定义的额外逻辑(如日志、缓存)和原函数。
比如一个简单的装饰器:
def my_decorator(func):print("执行装饰器(装饰阶段)")def wrapper():print("执行装饰器逻辑(调用阶段)")func()return wrapperdef test():print("执行原函数")# 此时还没调用test(),但会先打印“执行装饰器(装饰阶段)”# 调用test()时,才会打印“执行装饰器逻辑(调用阶段)”和“执行原函数”
理解 “装饰阶段” 和 “调用阶段” 的分离,是搞懂多装饰器顺序的前提。
我们通过自定义两个简单的装饰器,用 “打印日志” 的方式,直观验证多装饰器在 “装饰阶段” 和 “调用阶段” 的执行顺序。
步骤 1:定义两个装饰器
先定义decorator1和decorator2,每个装饰器都在 “装饰阶段” 和 “调用阶段” 打印明显的日志,方便区分:
# 装饰器1:打印装饰阶段和调用阶段的日志def decorator1(func):print("=== 执行decorator1的装饰逻辑(装饰阶段)===")def wrapper():print("=== 执行decorator1的前置逻辑(调用阶段)===")func() # 调用下一层函数(可能是decorator2的wrapper,或原函数)print("=== 执行decorator1的后置逻辑(调用阶段)===")return wrapper# 装饰器2:结构与decorator1一致,日志区分标识def decorator2(func):print("=== 执行decorator2的装饰逻辑(装饰阶段)===")def wrapper():print("=== 执行decorator2的前置逻辑(调用阶段)===")func() # 调用下一层函数(原函数)print("=== 执行decorator2的后置逻辑(调用阶段)===")return wrapper步骤 2:给函数加两个装饰器用@decorator1和@decorator2装饰test_func函数,观察执行结果:# 先写@decorator1,再写@decorator2(注意顺序)def test_func():print("=== 执行原函数test_func ===")# 此时代码刚加载,还没调用test_func(),先看“装饰阶段”的输出
执行结果(装饰阶段):
=== 执行decorator2的装饰逻辑(装饰阶段)===
=== 执行decorator1的装饰逻辑(装饰阶段)===
关键结论(装饰阶段):
多个装饰器的装饰顺序是 “从下到上”(即 “靠近原函数的装饰器先执行”):
先执行@decorator2(靠近原函数)的装饰逻辑;
再执行@decorator1(远离原函数)的装饰逻辑。
可以理解为:装饰器的写法顺序是@d1在上、@d2在下,但装饰时是d2先 “包裹” 原函数,d1再 “包裹”d2的结果,形成 “d1→d2→原函数” 的嵌套结构。
步骤 3:调用被装饰后的函数
现在调用test_func(),观察 “调用阶段” 的执行顺序:
# 调用被装饰后的test_func()print("\n开始调用test_func():")test_func()
执行结果(调用阶段):
开始调用test_func():
=== 执行decorator1的前置逻辑(调用阶段)===
=== 执行decorator2的前置逻辑(调用阶段)===
=== 执行原函数test_func ===
=== 执行decorator2的后置逻辑(调用阶段)===
=== 执行decorator1的后置逻辑(调用阶段)===
关键结论(调用阶段):
多个装饰器的调用顺序是 “从上到下”(即 “远离原函数的装饰器先执行前置逻辑,后执行后置逻辑”):
1.先执行decorator1的前置逻辑;
2.再执行decorator2的前置逻辑;
3.然后执行原函数;
4.接着执行decorator2的后置逻辑;
5.最后执行decorator1的后置逻辑。
形象理解:调用时就像 “剥洋葱”,先剥开最外层的decorator1,再剥开decorator2,执行原函数后,再一层层 “包回去”。
多个装饰器的执行顺序,本质是 “函数嵌套” 和 “函数调用栈” 的逻辑。我们用代码还原装饰过程,就能明白背后的原理。
1. 装饰阶段:本质是 “多层函数嵌套”
当我们写:
def test_func():pass
等价于 Python 解释器执行:
# 第一步:用decorator2装饰原函数test_func,得到d2_wrapperd2_wrapper = decorator2(test_func)# 第二步:用decorator1装饰d2_wrapper,得到d1_wrapper(最终的test_func)test_func = decorator1(d2_wrapper)
所以装饰阶段的顺序是 “先 decorator2,再 decorator1”—— 先完成内层嵌套,再完成外层嵌套。
2. 调用阶段:本质是 “多层函数调用栈”
调用test_func()时,实际调用的是d1_wrapper,执行流程如下:
# 调用test_func() → 即调用d1_wrapper()def d1_wrapper():print("d1前置")# 调用d2_wrapper()d2_wrapper()print("d1后置")# d2_wrapper()的执行def d2_wrapper():print("d2前置")# 调用原test_func()test_func()print("d2后置")# 原test_func()的执行def test_func():print("原函数")
所以调用阶段的顺序是 “d1 前置→d2 前置→原函数→d2 后置→d1 后置”—— 先执行外层装饰器的前置逻辑,再深入内层,最后从内层向外层执行后置逻辑。
上面的例子是 “无参数装饰器”,如果是 “带参数的装饰器”(如@decorator(arg)),执行顺序会变吗?我们再做一次验证。
步骤 1:定义带参数的装饰器
# 带参数的装饰器1:需先接收参数,再返回装饰器函数def decorator_with_arg1(arg):print(f"=== 执行decorator_with_arg1的参数处理(装饰阶段,参数:{arg})===")def actual_decorator(func):print("=== 执行decorator_with_arg1的实际装饰逻辑(装饰阶段)===")def wrapper():print("=== 执行decorator_with_arg1的前置逻辑(调用阶段)===")func()print("=== 执行decorator_with_arg1的后置逻辑(调用阶段)===")return wrapperreturn actual_decorator# 带参数的装饰器2:结构与1一致def decorator_with_arg2(arg):print(f"=== 执行decorator_with_arg2的参数处理(装饰阶段,参数:{arg})===")def actual_decorator(func):print("=== 执行decorator_with_arg2的实际装饰逻辑(装饰阶段)===")def wrapper():print("=== 执行decorator_with_arg2的前置逻辑(调用阶段)===")func()print("=== 执行decorator_with_arg2的后置逻辑(调用阶段)===")return wrapperreturn actual_decorator
步骤 2:装饰函数并验证
# 带参数的装饰器,顺序依然是@d1在上,@d2在下def test_arg_func():print("=== 执行带参数装饰器的原函数 ===")# 先看装饰阶段输出,再调用函数看调用阶段输出print("\n开始调用test_arg_func():")test_arg_func()
执行结果(装饰阶段):
=== 执行decorator_with_arg1的参数处理(装饰阶段,参数:hello)===
=== 执行decorator_with_arg2的参数处理(装饰阶段,参数:world)===
=== 执行decorator_with_arg2的实际装饰逻辑(装饰阶段)===
=== 执行decorator_with_arg1的实际装饰逻辑(装饰阶段)===
执行结果(调用阶段):
开始调用test_arg_func():
=== 执行decorator_with_arg1的前置逻辑(调用阶段)===
=== 执行decorator_with_arg2的前置逻辑(调用阶段)===
=== 执行带参数装饰器的原函数 ===
=== 执行decorator_with_arg2的后置逻辑(调用阶段)===
=== 执行decorator_with_arg1的后置逻辑(调用阶段)===
关键结论(带参数装饰器):
1.参数处理阶段:顺序是 “从上到下”(先执行上层装饰器的参数处理,再执行下层);
2.实际装饰阶段:顺序仍是 “从下到上”(先执行下层装饰器的实际装饰逻辑);
3.调用阶段:顺序仍是 “从上到下”(先执行上层装饰器的前置逻辑)。
本质是:带参数的装饰器多了 “参数处理步骤”(先执行decorator_with_arg(arg)得到实际装饰器),但后续的 “实际装饰” 和 “调用” 顺序与无参数装饰器一致。
1. 错误 1:混淆 “装饰顺序” 和 “调用顺序”
问题:误以为装饰顺序和调用顺序一致(比如认为 “@d1 在上,就先装饰 d1”),导致装饰逻辑执行不符合预期。
解决方案:记住 “两阶段” 规律:
装饰阶段:从下到上(靠近原函数的先装饰);
调用阶段:从上到下(远离原函数的先执行前置逻辑)。
2. 错误 2:带参数装饰器的参数处理顺序搞错
问题:在带参数装饰器中,误以为参数处理顺序是 “从下到上”,导致参数依赖逻辑出错。
解决方案:带参数装饰器的 “参数处理” 是 “从上到下”(先处理上层装饰器的参数),但 “实际装饰” 仍按 “从下到上”。
3. 错误 3:装饰器嵌套导致原函数元信息丢失
问题:多个装饰器嵌套后,func.__name__、func.__doc__等元信息丢失,调试困难。
解决方案:在每个装饰器的wrapper函数上添加@functools.wraps(func),保留原函数元信息。
import functoolsdef decorator1(func):# 关键:保留原函数元信息def wrapper():func()return wrapperdef decorator2(func):# 每个装饰器都要加def wrapper():func()return wrapperdef test():"""测试函数"""passprint(test.__name__) # 输出test(而非wrapper)print(test.__doc__) # 输出“测试函数”(而非空)
为了方便记忆,我们将多个装饰器的执行顺序总结成一张表:
终极记忆口诀:
装饰阶段 “从下到上” 包,调用阶段 “从上到下” 跑;
带参装饰先处理(参数),顺序依然 “上到下”。
掌握多个装饰器的执行顺序,能帮你在实际开发中灵活组合装饰器(如 “日志 + 缓存 + 权限校验”),让代码既简洁又符合逻辑。比如给接口函数加 “权限校验装饰器(@auth)” 和 “日志装饰器(@log)”,按@log在上、@auth在下的顺序,就能实现 “先记录日志,再校验权限” 的调用逻辑(符合业务场景)。
如果在实际使用中遇到其他问题,欢迎在评论区留言,我们一起探讨!

