好的,以下是对 多个装饰器一起使用 的更详细总结,包含原理图解、执行流程分析、顺序影响、带参数装饰器叠加、保留元信息、常见错误以及实战示例。
多个装饰器叠加使用(详细版)
一、基本语法与等价形式
@deco1
@deco2
@deco3
def func():
pass
完全等价于:
func = deco1( deco2( deco3(func) ) )
- 装饰器书写顺序:从上到下(
deco1→deco2→deco3) - 装饰器调用顺序:从下到上(先
deco3,再deco2,最后deco1)
二、装饰阶段与调用阶段(详细)
2.1 装饰阶段(定义函数时)
当 Python 执行到 @deco1 等语句时,它立即计算装饰器表达式,并从最内层开始逐层包装。
步骤分解(以 @deco1 @deco2 @deco3 func 为例):
- 计算
deco3(func),得到wrapped3(假设deco3返回一个包装函数)。 - 计算
deco2(wrapped3),得到wrapped2。 - 计算
deco1(wrapped2),得到wrapped1。 - 将名称
func绑定到wrapped1。
结果:func 现在指向最外层装饰器 deco1 返回的包装函数。
2.2 调用阶段(执行 func() 时)
此时实际执行的是最外层包装函数 wrapped1。其内部结构大致如下:
# wrapped1 内部(由 deco1 返回)
def wrapped1(*args, **kwargs):
# deco1 的前置代码
result = wrapped2(*args, **kwargs) # wrapped2 是 deco2 返回的包装函数
# deco1 的后置代码
return result
而 wrapped2 内部又会调用 wrapped3,wrapped3 内部调用原始 func。
执行流程就像一个“洋葱”:从最外层进入,逐层深入,到达最内层(原函数),然后逐层退出。
三、执行顺序图解(洋葱模型)
调用 func()
│
▼
┌─────────────────────────────┐
│ deco1 的前置代码(先执行) │
│ ┌─────────────────────────┐ │
│ │ deco2 的前置代码 │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ deco3 的前置代码 │ │ │
│ │ │ ┌───────────────┐ │ │ │
│ │ │ │ 原始 func │ │ │ │
│ │ │ └───────────────┘ │ │ │
│ │ │ deco3 的后置代码 │ │ │
│ │ └─────────────────────┘ │ │
│ │ deco2 的后置代码 │ │
│ └─────────────────────────┘ │
│ deco1 的后置代码(最后执行) │
└─────────────────────────────┘
输出顺序(假设每个装饰器打印“开始”和“结束”):
deco1 开始deco2 开始deco3 开始原始函数执行deco3 结束deco2 结束deco1 结束
四、完整示例(带打印追踪)
def deco1(func):
def wrapper(*args, **kwargs):
print(">>> deco1: 前置代码")
result = func(*args, **kwargs)
print("<<< deco1: 后置代码")
return result
return wrapper
def deco2(func):
def wrapper(*args, **kwargs):
print(" >>> deco2: 前置代码")
result = func(*args, **kwargs)
print(" <<< deco2: 后置代码")
return result
return wrapper
def deco3(func):
def wrapper(*args, **kwargs):
print(" >>> deco3: 前置代码")
result = func(*args, **kwargs)
print(" <<< deco3: 后置代码")
return result
return wrapper
@deco1
@deco2
@deco3
def say_hello():
print(" [原函数] Hello!")
say_hello()
输出:
>>> deco1: 前置代码
>>> deco2: 前置代码
>>> deco3: 前置代码
[原函数] Hello!
<<< deco3: 后置代码
<<< deco2: 后置代码
<<< deco1: 后置代码
(缩进只是为了视觉层次)
五、装饰器顺序对结果的影响
顺序不同,行为可能完全不同。举例:权限校验 + 日志记录
5.1 正确的顺序(先校验,后记录)
@log # 2. 记录调用日志
@require_login # 1. 先检查登录
def profile():
pass
等价于:profile = log( require_login(profile) )
- 调用时先执行
log的前置代码(记录开始时间等),然后调用require_login包装的函数,require_login先校验登录,通过后才调用原函数。
5.2 错误的顺序(先记录,后校验)
@require_login
@log
def profile():
pass
等价于:profile = require_login( log(profile) )
- 调用时先执行
require_login的前置代码(校验登录),但此时还没有记录日志。如果登录失败,日志就不会被记录,可能丢失审计信息。
结论:根据功能逻辑选择顺序,通常“核心功能”在内层,“外围增强”在外层。
六、带参数的多个装饰器叠加
带参数的装饰器本身是三层嵌套,叠加时仍然遵循从下到上的装饰顺序。
示例:重试 + 超时
import time
import functools
def retry(times=3):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for i in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
if i == times - 1:
raise e
print(f"重试 {i+1}")
return wrapper
return decorator
def timeout(seconds):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
import signal
def handler(signum, frame):
raise TimeoutError(f"执行超时 {seconds} 秒")
signal.signal(signal.SIGALRM, handler)
signal.alarm(seconds)
try:
return func(*args, **kwargs)
finally:
signal.alarm(0)
return wrapper
return decorator
@retry(times=2)
@timeout(seconds=1)
def risky_task():
time.sleep(2)
return "Done"
# 调用时:
# 1. retry 的外层包装先执行
# 2. 进入 retry 内部后,调用 timeout 返回的包装函数
# 3. timeout 内部设置闹钟,然后调用原函数
# 4. 原函数超时抛出 TimeoutError,被 retry 捕获并重试一次
# 5. 第二次依然超时,最终抛出异常
装饰顺序:先 @timeout 装饰原函数,再 @retry 装饰结果。
执行顺序:先进入 retry 的包装,再进入 timeout 的包装。
七、保留元信息:functools.wraps
多个装饰器叠加后,原函数的 __name__、__doc__ 等属性会丢失,变成最外层包装函数的属性。使用 @functools.wraps(func) 可以解决。
未使用 wraps
def deco1(func):
def wrapper():
"""wrapper doc"""
return func()
return wrapper
@deco1
def hello():
"""hello doc"""
pass
print(hello.__name__) # 输出 'wrapper'
print(hello.__doc__) # 输出 'wrapper doc'
使用 wraps
import functools
def deco1(func):
@functools.wraps(func)
def wrapper():
"""wrapper doc"""
return func()
return wrapper
@deco1
def hello():
"""hello doc"""
pass
print(hello.__name__) # 输出 'hello'
print(hello.__doc__) # 输出 'hello doc'
建议:每个装饰器内部都加上 @functools.wraps(func),这样叠加多个后,原函数的信息依然保留,对调试和文档生成很有帮助。
八、常见错误与注意事项
8.1 装饰器忘记返回包装函数
def deco(func):
def wrapper():
return func()
# 忘记 return wrapper → 原函数被替换为 None
8.2 顺序误解
以为 @deco1 @deco2 就是先执行 deco1 再执行 deco2。
正确:装饰阶段先 deco2 后 deco1;调用阶段先 deco1 的前置,再 deco2 的前置。
8.3 带参数装饰器少写一层
def retry(times): # 缺少内层 decorator 函数
def wrapper(func):
...
return wrapper # 实际上 retry 直接返回了 wrapper,少了一层
标准带参数装饰器需要三层:外层接收参数,中层接收函数,内层接收调用参数。
8.4 多个装饰器同时修改返回值
如果装饰器修改了返回值(例如将字符串转为大写),后面的装饰器将基于修改后的值操作,可能产生意外。
def upper_result(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
def add_exclamation(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs) + "!"
return wrapper
@add_exclamation
@upper_result
def greet():
return "hello"
print(greet()) # 输出 "HELLO!" (先转大写,再加感叹号)
若交换顺序:@upper_result @add_exclamation 则先加感叹号再转大写 → "HELLO!" 相同(因为大写不影响感叹号)。但若涉及数字等类型转换则可能出错。
九、实际应用场景举例
| 组合 | 说明 |
|---|---|
@cache + @log | 先缓存,缓存命中时不记录日志?通常先记录日志再查缓存,以便追踪调用。 |
@login_required + @admin_required | 先检查是否登录,再检查是否是管理员。 |
@retry + @timeout | 先设置超时,再重试(超时异常可以被重试捕获)。 |
@flask_route + @jwt_required | Web 框架中先匹配路由,再验证 JWT。 |
十、总结一句话
装饰器叠加时,书写顺序从上到下,装饰顺序从下到上;调用时从最外层进入,逐层深入原函数,再逐层退出,如同洋葱。
掌握这个模型,无论多少个装饰器、带不带参数,都能准确预测执行流程。