Python中的函数装饰器高级应用:带参数装饰器、类装饰器与装饰器堆叠
题目描述
函数装饰器是Python中用于修改或增强函数行为的工具。在掌握基础装饰器后,面试常考察其高级应用,包括带参数的装饰器、类装饰器和多层装饰器堆叠。这些知识点涉及闭包、函数嵌套、参数传递、描述符协议和装饰器执行顺序等核心概念。
步骤1:复习基础装饰器原理
装饰器本质上是一个可调用对象(函数或类),它接收一个函数作为参数,并返回一个新函数(或可调用对象)。装饰器语法 @decorator 是语法糖,等价于 func = decorator(func)。
示例:一个简单的计时装饰器
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 执行时间: {end - start:.2f}秒")
return result
return wrapper
@timer
def compute(n):
return sum(i * i for i in range(n))
compute(10000) # 输出: compute 执行时间: 0.00秒
关键点:timer 接收函数 compute,返回闭包函数 wrapper,wrapper 在原函数前后添加计时逻辑。
步骤2:带参数的装饰器实现
当装饰器需要接收外部参数(如日志级别、重试次数)时,需在原有装饰器外再包装一层函数,形成三层嵌套结构。
示例:带重试机制的装饰器
def retry(max_attempts=3, delay=1):
# 第一层:接收装饰器参数
def decorator(func):
# 第二层:接收被装饰函数
def wrapper(*args, **kwargs):
# 第三层:包装逻辑
last_error = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_error = e
print(f"第{attempt}次失败,错误: {e}")
time.sleep(delay)
raise Exception(f"重试{max_attempts}次后失败") from last_error
return wrapper
return decorator
@retry(max_attempts=2, delay=0.5)
def risky_call():
import random
if random.random() < 0.7:
raise ValueError("随机失败")
return "成功"
try:
print(risky_call())
except Exception as e:
print(f"最终失败: {e}")
执行过程:
@retry(max_attempts=2, delay=0.5)先调用retry(2, 0.5),返回decorator函数。decorator接收函数risky_call,返回wrapper函数。- 调用
risky_call()实际执行wrapper(),实现重试逻辑。
步骤3:类装饰器实现
类装饰器利用类的 __init__ 和 __call__ 方法实现装饰逻辑。__init__ 接收被装饰函数,__call__ 使得实例可调用,替代装饰后的函数。
示例:记录函数调用次数的类装饰器
class CallCounter:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} 被调用第{self.count}次")
return self.func(*args, **kwargs)
@CallCounter
def greet(name):
return f"Hello, {name}"
print(greet("Alice")) # 输出: greet 被调用第1次 → Hello, Alice
print(greet("Bob")) # 输出: greet 被调用第2次 → Hello, Bob
关键点:CallCounter 实例化时保存原函数,调用实例时触发 __call__ 方法,实现装饰逻辑。类装饰器相比函数装饰器更易于维护状态(如 count)。
步骤4:多层装饰器堆叠与执行顺序
多个装饰器可以堆叠在一个函数上,执行顺序为从下到上(或从内到外),但调用时的执行顺序是从上到下。
示例:堆叠装饰器
def decorator_a(func):
def wrapper(*args, **kwargs):
print("装饰器A - 前")
result = func(*args, **kwargs)
print("装饰器A - 后")
return result
return wrapper
def decorator_b(func):
def wrapper(*args, **kwargs):
print("装饰器B - 前")
result = func(*args, **kwargs)
print("装饰器B - 后")
return result
return wrapper
@decorator_a
@decorator_b
def target():
print("目标函数执行")
target()
输出:
装饰器A - 前
装饰器B - 前
目标函数执行
装饰器B - 后
装饰器A - 后
执行过程:
- 装饰顺序:
target = decorator_a(decorator_b(target)),即先应用decorator_b,再应用decorator_a。 - 调用时:从最外层的
decorator_a.wrapper开始执行,依次向内层传递,最后执行原函数,再向外层返回。
步骤5:装饰器常见问题与解决方案
-
问题1:被装饰函数的元信息(如
__name__、__doc__)丢失
解决:使用functools.wraps装饰内部函数,将原函数的元信息复制到包装函数。from functools import wraps def my_decorator(func): @wraps(func) def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper -
问题2:装饰器在类方法上失效
原因:类方法的第一个参数是self,但装饰器可能未正确处理。
解决:在装饰器内部使用*args, **kwargs通用参数传递,或使用functools.wraps保持方法绑定。 -
问题3:装饰器调试困难
建议:为装饰器添加日志,或使用inspect模块打印调用信息。
总结
- 带参数装饰器:三层嵌套函数,外层接收装饰器参数,中层接收被装饰函数,内层实现装饰逻辑。
- 类装饰器:通过
__init__和__call__实现,便于维护状态。 - 装饰器堆叠:装饰顺序从下到上,执行顺序从上到下,类似洋葱模型。
- 最佳实践:使用
functools.wraps保留元信息,注意装饰器对性能的影响。
这些高级装饰器技巧广泛应用于Web框架(如Flask路由)、测试框架(如pytest夹具)、日志记录和性能监控等场景。