Python 装饰器完全指南:从基础到实战应用的深度解析
一、装饰器的基本原理
装饰器的本质是一个接收函数作为参数,并返回一个新函数的高阶函数。Python 使用 @ 语法糖来应用装饰器,这让代码更加简洁易读。
让我们从一个最简单的计时装饰器开始,理解装饰器的工作机制:
import time
from functools import wraps
def timer_decorator(func):
"""记录函数执行时间的装饰器"""
@wraps(func) # 保留原函数的元信息
def wrapper(*args, **kwargs):
start_time = time.time()
result = func(*args, **kwargs)
end_time = time.time()
print(f"{func.__name__} 执行耗时: {end_time - start_time:.4f}秒")
return result
return wrapper
# 使用装饰器
@timer_decorator
def calculate_sum(n):
"""计算 1 到 n 的和"""
total = 0
for i in range(1, n + 1):
total += i
return total
# 调用函数
result = calculate_sum(1000000)
print(f"计算结果: {result}")
上面的代码展示了装饰器的基本结构。当我们使用 @timer_decorator 装饰 calculate_sum 函数时,实际上等同于:
calculate_sum = timer_decorator(calculate_sum)
关键点说明:
@wraps(func)是一个重要的工具,它保留了被装饰函数的__name__、__doc__等元信息,避免调试时产生混淆。- wrapper 函数接收任意参数
*args, **kwargs,确保装饰器适用于各种签名的函数。 - wrapper 在调用原函数前后执行额外逻辑,最后返回原函数的执行结果。
二、带参数的装饰器
有时候我们希望装饰器能够接收参数,根据不同的参数表现出不同的行为。这需要创建一个返回装饰器的工厂函数。
下面的例子展示了一个可配置的重试装饰器:
import random
def retry_decorator(max_attempts=3, delay=1.0):
"""
函数执行失败时自动重试的装饰器
Args:
max_attempts: 最大重试次数
delay: 重试之间的延迟时间(秒)
"""
import time
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
attempts = 0
last_exception = None
while attempts < max_attempts:
try:
return func(*args, **kwargs)
except Exception as e:
attempts += 1
last_exception = e
if attempts < max_attempts:
print(f"第 {attempts} 次尝试失败,{delay} 秒后重试...")
time.sleep(delay)
# 所有尝试都失败,抛出最后一次异常
raise Exception(f"函数执行失败,已尝试 {max_attempts} 次") from last_exception
return wrapper
return decorator
# 模拟一个可能失败的函数
@retry_decorator(max_attempts=5, delay=0.5)
def unstable_api_call():
"""模拟不稳定的 API 调用"""
if random.random() > 0.7: # 30% 成功率
return "API 调用成功!"
raise ConnectionError("网络连接失败")
try:
result = unstable_api_call()
print(f"最终结果: {result}")
except Exception as e:
print(f"最终失败: {e}")
带参数装饰器的结构是三层函数:
- 外层函数(retry_decorator):接收装饰器参数,返回装饰器函数
- 中层函数(decorator):接收被装饰函数,返回 wrapper 函数
- 内层函数(wrapper):实际执行逻辑的包装函数
三、类装饰器
装饰器不仅可以应用于函数,还可以应用于类。类装饰器可以动态地修改类的属性和方法,或者创建新的类。
def add_class_methods(cls):
"""为类添加额外方法的装饰器"""
def to_dict(self):
"""将对象转为字典"""
return {k: v for k, v in self.__dict__.items() if not k.startswith('_')}
def from_dict(cls, data):
"""从字典创建类实例"""
instance = cls()
for k, v in data.items():
setattr(instance, k, v)
return instance
# 动态添加方法
cls.to_dict = to_dict
cls.from_dict = classmethod(from_dict)
return cls
@add_class_methods
class User:
"""用户类"""
def __init__(self, name=None, email=None):
self.name = name
self.email = email
# 使用新增的方法
user = User(name="张三", email="zhangsan@example.com")
print(user.to_dict())
user_data = {"name": "李四", "email": "lisi@example.com"}
user2 = User.from_dict(user_data)
print(user2.to_dict())
类装饰器在框架开发中非常有用,例如 Django 的 @model 装饰器、SQLAlchemy 的 @declarative_base 等。
四、装饰器链
Python 允许我们对一个函数应用多个装饰器,装饰器会按照从下到上的顺序执行。这被称为装饰器链。
def log_args(func):
"""记录函数调用参数的装饰器"""
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[调用日志] {func.__name__} 参数: args={args}, kwargs={kwargs}")
return func(*args, **kwargs)
return wrapper
def validate_positive(func):
"""验证参数为正数的装饰器"""
@wraps(func)
def wrapper(*args, **kwargs):
# 检查所有参数
for arg in args:
if isinstance(arg, (int, float)) and arg <= 0:
raise ValueError(f"参数必须为正数,实际值: {arg}")
for value in kwargs.values():
if isinstance(value, (int, float)) and value <= 0:
raise ValueError(f"参数必须为正数,实际值: {value}")
return func(*args, **kwargs)
return wrapper
# 使用装饰器链
@log_args
@validate_positive
def calculate_discount(price, discount_rate):
"""计算折扣后价格"""
return price * (1 - discount_rate)
# 测试
result = calculate_discount(100, 0.2) # 80.0
print(f"折扣后价格: {result}")
try:
calculate_discount(-100, 0.2) # 会抛出异常
except ValueError as e:
print(f"验证失败: {e}")
装饰器链执行顺序:上面的例子中,当调用 calculate_discount 时,执行顺序是:
- log_args 的 wrapper 先执行(最外层)
- validate_positive 的 wrapper 次执行(中间层)
- 原函数 calculate_discount 最后执行
五、实战案例:缓存装饰器
缓存是装饰器的经典应用场景,可以显著提升重复计算密集型任务的性能。
import hashlib
def memoize(max_size=100):
"""
缓存函数结果的装饰器
Args:
max_size: 最大缓存条目数
"""
def decorator(func):
cache = {} # 缓存字典
@wraps(func)
def wrapper(*args, **kwargs):
# 生成缓存键
cache_key = hashlib.md5(
str((args, tuple(sorted(kwargs.items())))).encode()
).hexdigest()
# 检查缓存
if cache_key in cache:
print(f"[缓存命中] {func.__name__}")
return cache[cache_key]
# 计算结果并缓存
result = func(*args, **kwargs)
cache[cache_key] = result
# 限制缓存大小(简单的 FIFO 策略)
if len(cache) > max_size:
oldest_key = next(iter(cache))
del cache[oldest_key]
print(f"[缓存存储] {func.__name__}")
return result
wrapper.cache = cache # 暴露缓存以便外部操作
return wrapper
return decorator
# 斐波那契数列计算(经典的重复计算问题)
@memoize(max_size=50)
def fibonacci(n):
"""计算斐波那契数列第 n 项"""
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
# 测试缓存效果
import time
start = time.time()
result1 = fibonacci(35)
end = time.time()
print(f"fibonacci(35) = {result1}, 首次耗时: {end - start:.4f}秒")
start = time.time()
result2 = fibonacci(35)
end = time.time()
print(f"fibonacci(35) = {result2}, 再次耗时: {end - start:.4f}秒 (从缓存读取)")
六、实战案例:权限验证装饰器
在 Web 开发中,装饰器常用于权限验证。下面的例子模拟了一个简单的权限系统:
class PermissionDenied(Exception):
"""权限拒绝异常"""
pass
def require_permission(*required_roles):
"""
权限验证装饰器
Args:
required_roles: 要求的角色列表
"""
def decorator(func):
@wraps(func)
def wrapper(self, *args, **kwargs):
# 假设用户角色存储在 self.current_user 中
user_roles = getattr(self, 'current_user_roles', [])
# 检查是否拥有任一所需角色
has_permission = any(role in user_roles for role in required_roles)
if not has_permission:
raise PermissionDenied(
f"需要权限: {required_roles}, 当前角色: {user_roles}"
)
return func(self, *args, **kwargs)
return wrapper
return decorator
class AdminPanel:
"""管理面板"""
def __init__(self, user_roles):
self.current_user_roles = user_roles
@require_permission('admin')
def delete_user(self, user_id):
"""删除用户(需要管理员权限)"""
return f"用户 {user_id} 已删除"
@require_permission('admin', 'moderator')
def ban_user(self, user_id):
"""封禁用户(管理员或版主权限)"""
return f"用户 {user_id} 已被封禁"
# 测试权限验证
admin_panel = AdminPanel(['admin', 'user'])
print(admin_panel.delete_user(123)) # 成功
print(admin_panel.ban_user(456)) # 成功
moderator_panel = AdminPanel(['moderator'])
print(moderator_panel.ban_user(789)) # 成功
try:
moderator_panel.delete_user(999) # 失败,没有 admin 权限
except PermissionDenied as e:
print(f"权限错误: {e}")
七、最佳实践与注意事项
1. 始终使用 functools.wraps
这保留原函数的元信息,对于调试、文档生成和函数序列化至关重要。
2. 装饰器应该是可组合的
好的装饰器设计应该不影响其他装饰器的正常工作,避免产生意外依赖。
3. 注意装饰器的性能影响
装饰器会引入额外的函数调用开销,在性能敏感的场景中需要谨慎使用。
4. 适当使用类作为装饰器
对于有状态的装饰器(如缓存、计数器),使用类实现可能更清晰:
class CountCalls:
"""统计函数调用次数的装饰器"""
def __init__(self, func):
self.func = func
self.call_count = 0
wraps(func)(self)
def __call__(self, *args, **kwargs):
self.call_count += 1
print(f"{self.func.__name__} 已被调用 {self.call_count} 次")
return self.func(*args, **kwargs)
@CountCalls
def process_data(data):
"""处理数据"""
return data * 2
process_data(10)
process_data(20)
process_data(30)
八、总结
Python 装饰器是一项强大而优雅的工具,它体现了 Python "简洁即美" 的设计哲学。通过装饰器,我们可以:
- 分离关注点:将核心业务逻辑与横切关注点(日志、缓存、权限等)分离
- 提升代码复用:一次编写装饰器,多处复用
- 保持代码简洁:避免重复的样板代码
- 增强可维护性:修改功能只需调整装饰器,无需修改业务代码
在实际项目中,装饰器广泛应用于日志记录、性能监控、缓存、权限验证、事务管理等场景。掌握装饰器的进阶用法,将让你的 Python 代码更加优雅和高效。
进一步学习:
- Python 的
functools.lru_cache提供了内置的缓存装饰器 - 研究 Flask、Django 等框架中的装饰器实现
- 探索异步函数装饰器(async/await)的特殊处理
