Python 装饰器:从原理到实战应用的完整指南
装饰器的核心思想
装饰器本质上是一个接受函数作为参数的函数,它返回一个新的函数。这个新函数通常会包装原函数,在执行前后添加额外的逻辑。装饰器的语法糖 @ 让代码更加简洁优雅。
为什么需要装饰器?
想象一个场景:你正在开发一个大型系统,有几十个函数需要记录执行时间。传统的做法是在每个函数内部添加计时逻辑,但这会导致代码重复,违反 DRY(Don't Repeat Yourself)原则。装饰器提供了一个优雅的解决方案:将计时逻辑封装一次,然后应用到任何需要的函数上。
基础装饰器实现
import time
from functools import wraps
def timer(func):
"""计算函数执行时间的装饰器"""
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
print(f"{func.__name__} 执行耗时: {end_time - start_time:.6f} 秒")
return result
return wrapper
@timer
def calculate_fibonacci(n):
"""计算斐波那契数列第 n 项"""
if n <= 1:
return n
return calculate_fibonacci(n-1) + calculate_fibonacci(n-2)
# 执行测试
calculate_fibonacci(30)
functools.wraps 的作用
在上面的代码中,我们使用了 @wraps(func)。这是一个非常重要的细节。装饰器会替换原函数,如果不使用 wraps,新函数的 __name__、__doc__ 等属性会被覆盖。wraps 装饰器会将原函数的元数据复制到 wrapper 函数上,保持函数的"身份"不变。
带参数的装饰器
有时装饰器需要接受参数,比如重试装饰器需要指定重试次数和延迟时间。这种情况下,我们需要创建一个装饰器工厂函数。
import time
from functools import wraps
def retry(max_attempts=3, delay=1):
"""带参数的重试装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(max_attempts):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
print(f"第 {attempt + 1} 次尝试失败: {e}")
if attempt < max_attempts - 1:
time.sleep(delay)
raise last_exception
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def fetch_data(url):
"""模拟可能失败的数据获取"""
import random
if random.random() < 0.7:
raise ConnectionError("网络连接失败")
return f"数据来自 {url}"
# 执行测试
try:
data = fetch_data("https://api.example.com/data")
print(f"成功获取数据: {data}")
except Exception as e:
print(f"最终失败: {e}")
类装饰器
除了函数装饰器,Python 还支持类装饰器。类装饰器可以用来修改类的行为,比如添加方法、修改属性等。
def add_str_method(cls):
"""为类添加 __str__ 方法的装饰器"""
def __str__(self):
attrs = {k: v for k, v in self.__dict__.items() if not k.startswith('_')}
return f"{cls.__name__}({attrs})"
cls.__str__ = __str__
return cls
@add_str_method
class User:
def __init__(self, name, age, email):
self.name = name
self.age = age
self.email = email
# 执行测试
user = User("张三", 25, "zhangsan@example.com")
print(user) # 输出: User({'name': '张三', 'age': 25, 'email': 'zhangsan@example.com'})
装饰器栈:多个装饰器的组合
Python 允许在一个函数上应用多个装饰器,装饰器的执行顺序是从下往上(从内向外)。
import time
from functools import wraps
def log_calls(func):
"""记录函数调用的装饰器"""
@wraps(func)
def wrapper(*args, **kwargs):
print(f"调用函数: {func.__name__}")
result = func(*args, **kwargs)
print(f"函数 {func.__name__} 执行完成")
return result
return wrapper
def cache_results(func):
"""缓存结果的装饰器"""
cache = {}
@wraps(func)
def wrapper(*args, **kwargs):
key = (args, frozenset(kwargs.items()))
if key in cache:
print(f"从缓存中返回结果")
return cache[key]
result = func(*args, **kwargs)
cache[key] = result
return result
return wrapper
@log_calls
@cache_results
def expensive_computation(x):
"""耗时计算函数"""
print("执行复杂计算...")
time.sleep(0.1)
return x * x
# 执行测试
print(expensive_computation(5)) # 首次调用,执行计算
print(expensive_computation(5)) # 第二次调用,从缓存返回
print(expensive_computation(10)) # 新参数,执行计算
单例模式的装饰器实现
装饰器在实现设计模式时非常有用,比如单例模式。
def singleton(cls):
"""单例装饰器"""
instances = {}
@wraps(cls)
def get_instance(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return get_instance
@singleton
class DatabaseConnection:
def __init__(self):
print("创建数据库连接")
self.connected = False
def connect(self):
if not self.connected:
print("连接数据库...")
self.connected = True
else:
print("已经连接")
# 执行测试
db1 = DatabaseConnection()
db2 = DatabaseConnection()
print(f"db1 和 db2 是同一个对象: {db1 is db2}") # 输出: True
性能监控装饰器
在实际项目中,性能监控装饰器可以帮助我们快速定位性能瓶颈。
import time
from functools import wraps
from collections import defaultdict
class PerformanceMonitor:
def __init__(self):
self.call_times = defaultdict(list)
def monitor(self, func):
"""性能监控装饰器"""
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.perf_counter()
result = func(*args, **kwargs)
end_time = time.perf_counter()
duration = end_time - start_time
self.call_times[func.__name__].append(duration)
return result
return wrapper
def report(self):
"""生成性能报告"""
print("\n性能监控报告:")
print("-" * 40)
for func_name, times in self.call_times.items():
avg_time = sum(times) / len(times)
max_time = max(times)
min_time = min(times)
print(f"{func_name}:")
print(f" 调用次数: {len(times)}")
print(f" 平均耗时: {avg_time:.6f}s")
print(f" 最大耗时: {max_time:.6f}s")
print(f" 最小耗时: {min_time:.6f}s")
# 使用示例
monitor = PerformanceMonitor()
@monitor.monitor
def data_processing(data):
"""数据处理函数"""
time.sleep(0.01)
return [x * 2 for x in data]
@monitor.monitor
def save_to_disk(data):
"""保存数据到磁盘"""
time.sleep(0.005)
return len(data)
# 执行测试
for _ in range(5):
data = list(range(100))
processed = data_processing(data)
save_to_disk(processed)
monitor.report()
装饰器的最佳实践:
1. 始终使用 functools.wraps:保持原函数的元数据,这对于调试和文档生成非常重要。
2. 保持装饰器简单:复杂的装饰器难以维护和测试。如果装饰器逻辑太复杂,考虑将其分解为多个装饰器或使用类。
3. 文档化装饰器:为装饰器编写清晰的文档字符串,说明它的作用和参数。
4. 考虑性能影响:装饰器会增加函数调用的开销,对于性能敏感的代码要谨慎使用。
5. 处理可调用对象:如果装饰器需要应用到方法上,考虑使用描述符协议。
总结
装饰器是 Python 中体现"开放封闭原则"的完美示例——对扩展开放,对修改封闭。通过装饰器,我们可以在不修改原有代码的情况下,为函数和类添加新功能。这种特性使得代码更加模块化、可维护,也更符合 Python 哲学中的优雅和简洁。
掌握装饰器不仅能让你的代码更 Pythonic,还能帮助你更好地理解函数式编程思想、高阶函数和闭包等核心概念。在实际项目中,合理使用装饰器可以大大减少代码重复,提高开发效率和代码质量。
