一、什么是多个装饰器?
多个装饰器是指在一个函数上同时应用多个装饰器,每个装饰器为函数添加不同的功能。Python 允许将多个装饰器叠加使用,实现功能的组合与叠加。
defdecorator_a(func):
defwrapper(*args, **kwargs):
print("A 开始")
result = func(*args, **kwargs)
print("A 结束")
return result
return wrapper
defdecorator_b(func):
defwrapper(*args, **kwargs):
print("B 开始")
result = func(*args, **kwargs)
print("B 结束")
return result
return wrapper
@decorator_a
@decorator_b
defsay_hello():
print("Hello!")
say_hello()
输出:
A 开始
B 开始
Hello!
B 结束
A 结束
二、多个装饰器的执行顺序
多个装饰器的执行顺序遵循从下往上装饰,从上往下执行的规则。
2.1 装饰顺序(定义时)
装饰器在函数定义时从最靠近函数的开始向外应用:
@decorator_a
@decorator_b
@decorator_c
deffunc():
pass
# 等价于:
func = decorator_a(decorator_b(decorator_c(func)))
2.2 执行顺序(调用时)
当调用被装饰的函数时,执行顺序是从上往下:
defdecorator_one(func):
print("装饰器 one 被调用")
defwrapper(*args, **kwargs):
print("one 开始执行")
result = func(*args, **kwargs)
print("one 结束执行")
return result
return wrapper
defdecorator_two(func):
print("装饰器 two 被调用")
defwrapper(*args, **kwargs):
print("two 开始执行")
result = func(*args, **kwargs)
print("two 结束执行")
return result
return wrapper
@decorator_one
@decorator_two
defmy_func():
print("原函数执行")
print("--- 开始调用 ---")
my_func()
输出:
装饰器 two 被调用
装饰器 one 被调用
--- 开始调用 ---
one 开始执行
two 开始执行
原函数执行
two 结束执行
one 结束执行
三、装饰器应用顺序分析图
@decorator_a
@decorator_b
@decorator_c
def func():
pass
装饰阶段(定义时):
decorator_c(func) → wrapper_c
decorator_b(wrapper_c) → wrapper_b
decorator_a(wrapper_b) → wrapper_a
最终 func = wrapper_a
执行阶段(调用时):
wrapper_a() 开始
↓
wrapper_b() 开始
↓
wrapper_c() 开始
↓
原函数 func()
↓
wrapper_c() 结束
↓
wrapper_b() 结束
↓
wrapper_a() 结束
四、多个装饰器的常见应用场景
4.1 日志 + 计时
import time
from functools import wraps
deflog(func):
@wraps(func)
defwrapper(*args, **kwargs):
print(f"调用函数: {func.__name__}")
return func(*args, **kwargs)
return wrapper
deftimer(func):
@wraps(func)
defwrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 耗时: {end - start:.4f}秒")
return result
return wrapper
@log
@timer
defslow_function():
time.sleep(1)
return"完成"
print(slow_function())
4.2 缓存 + 重试
from functools import wraps
import time
defcache(func):
"""缓存装饰器"""
cache_dict = {}
@wraps(func)
defwrapper(*args):
if args in cache_dict:
print(f"缓存命中: {args}")
return cache_dict[args]
print(f"计算: {args}")
result = func(*args)
cache_dict[args] = result
return result
return wrapper
defretry(max_attempts=3, delay=1):
"""重试装饰器"""
defdecorator(func):
@wraps(func)
defwrapper(*args, **kwargs):
for attempt inrange(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"第 {attempt} 次尝试失败: {e}")
if attempt == max_attempts:
raise
time.sleep(delay)
returnNone
return wrapper
return decorator
@cache
@retry(max_attempts=3)
defunstable_calc(n):
import random
if random.random() < 0.5:
raise ValueError("随机失败")
return n * n
print(unstable_calc(5))
4.3 权限验证 + 日志
from functools import wraps
defrequire_permission(permission):
"""权限验证装饰器"""
defdecorator(func):
@wraps(func)
defwrapper(user, *args, **kwargs):
ifhasattr(user, 'permissions') and permission in user.permissions:
return func(user, *args, **kwargs)
raise PermissionError(f"用户 {user.name} 缺少权限: {permission}")
return wrapper
return decorator
defaudit_log(func):
"""审计日志装饰器"""
@wraps(func)
defwrapper(user, *args, **kwargs):
print(f"[AUDIT] {user.name} 调用了 {func.__name__}")
return func(user, *args, **kwargs)
return wrapper
classUser:
def__init__(self, name, permissions):
self.name = name
self.permissions = permissions
@audit_log
@require_permission("delete")
defdelete_file(user, filename):
print(f"{user.name} 删除了文件: {filename}")
admin = User("张三", ["delete", "read"])
normal = User("李四", ["read"])
delete_file(admin, "test.txt") # 成功
# delete_file(normal, "test.txt") # PermissionError
五、装饰器顺序的影响
装饰器的顺序非常重要,不同顺序会产生不同的效果。
5.1 缓存应该在重试之后还是之前?
# 场景1:缓存在外层(先检查缓存,再重试)
@cache
@retry
deffunc(): ...
# 场景2:重试在外层(先重试,再缓存)
@retry
@cache
deffunc(): ...
- • 场景1:缓存失败的结果也会被缓存,重试逻辑可能不会执行。
- • 场景2:每次缓存未命中时都会执行重试逻辑,但成功结果会被缓存。
选择原则:
- • 如果希望缓存的是最终成功的结果(包括重试后的),重试应该在内层。
- • 如果希望每次都重试,但缓存的是成功结果,缓存应该在内层。
5.2 计时应该在日志外层还是内层?
# 计时包含日志输出时间
@timer
@log
deffunc(): ...
# 日志包含计时时间
@log
@timer
deffunc(): ...
前者记录的是函数+日志的总时间,后者记录的是纯函数执行时间。
六、类装饰器的叠加
类装饰器也可以叠加使用。
classLogDecorator:
def__init__(self, func):
self.func = func
def__call__(self, *args, **kwargs):
print(f"Log: 调用 {self.func.__name__}")
returnself.func(*args, **kwargs)
classTimerDecorator:
def__init__(self, func):
self.func = func
def__call__(self, *args, **kwargs):
import time
start = time.time()
result = self.func(*args, **kwargs)
end = time.time()
print(f"Timer: {self.func.__name__} 耗时 {end-start:.4f}s")
return result
@LogDecorator
@TimerDecorator
defprocess_data():
print("处理数据...")
return"完成"
process_data()
七、使用 functools.partial 简化多层装饰器
当需要多次应用相同装饰器时,可以使用 partial 简化。
from functools import partial
defrepeat(times):
defdecorator(func):
defwrapper(*args, **kwargs):
for _ inrange(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
# 手动创建多个相同装饰器
@repeat(2)
@repeat(3)
@repeat(5)
defgreet():
print("Hello")
greet() # 会执行 2*3*5 = 30 次
# 更清晰的写法
repeat_30 = partial(repeat, 30)
@repeat_30
defgreet2():
print("Hello")
八、注意事项
- 1. 顺序敏感:多个装饰器的顺序会影响最终行为,需要根据需求仔细安排。
- 2. 元信息保留:每个装饰器都应该使用
@wraps(func),否则最外层装饰器会暴露内层装饰器的元信息。 - 3. 性能影响:每个装饰器都会增加函数调用开销,过多装饰器可能影响性能。
- 4. 调试困难:多层装饰器会让堆栈跟踪变得复杂,使用
@wraps 可以改善。 - 5. 参数传递:所有装饰器的 wrapper 函数都需要正确处理
*args, **kwargs,以确保参数能正确传递给原函数。
九、调试多个装饰器
from functools import wraps
defdebug_decorator(name):
"""用于调试的装饰器,显示装饰器应用顺序"""
defdecorator(func):
print(f"正在应用装饰器: {name} 到函数 {func.__name__}")
@wraps(func)
defwrapper(*args, **kwargs):
print(f"执行装饰器: {name} 开始")
result = func(*args, **kwargs)
print(f"执行装饰器: {name} 结束")
return result
return wrapper
return decorator
@debug_decorator("A")
@debug_decorator("B")
@debug_decorator("C")
defmy_func():
print("原函数执行")
print("--- 调用函数 ---")
my_func()
输出:
正在应用装饰器: C 到函数 my_func
正在应用装饰器: B 到函数 wrapper
正在应用装饰器: A 到函数 wrapper
--- 调用函数 ---
执行装饰器: A 开始
执行装饰器: B 开始
执行装饰器: C 开始
原函数执行
执行装饰器: C 结束
执行装饰器: B 结束
执行装饰器: A 结束
十、总结
| |
| |
| |
| func = decorator1(decorator2(decorator3(func))) |
| |
| |
核心理解:
- • 多个装饰器像洋葱一样层层包裹,调用时从外向内,返回时从内向外。
- • 装饰器顺序决定了功能的组合方式,不同顺序产生不同效果。
- • 每个装饰器都应使用
@functools.wraps 保留函数元信息。 - • 合理组合多个装饰器可以实现功能的模块化和复用。
掌握多个装饰器的叠加使用,可以让你用声明式的方式组合各种功能,写出更加清晰、可维护的代码。