Python 装饰器的高级用法:从基础到实战
装饰器(Decorator)是 Python 中最优雅和强大的特性之一。在日常开发中,我们经常使用 @staticmethod、@property 这样的内置装饰器,但你是否真正理解装饰器背后的工作原理?本文将带你深入装饰器的世界,从基础概念到高级用法,掌握这个能显著提升代码质量的工具。
一、装饰器的本质
装饰器的本质是一个高阶函数,它接受一个函数作为参数,并返回一个新的函数。最基础的装饰器可以理解为一个函数包装器,在不修改原函数代码的情况下,为其添加额外的功能。
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:.4f} 秒")
return result
return wrapper
@timer
def calculate_sum(n):
return sum(range(n))
# 调用函数时会自动计时
calculate_sum(1000000)
# 输出: calculate_sum 执行耗时: 0.0452 秒
这个例子展示了装饰器的基本工作原理:calculate_sum 函数被 timer 装饰器包装后,每次调用都会先记录开始时间,执行原函数,记录结束时间并打印耗时,最后返回原函数的结果。
二、保留元信息的装饰器
基础装饰器有个问题:被装饰函数的 __name__、__doc__ 等元信息会被替换成 wrapper 函数的信息。这在调试时会带来麻烦。Python 提供了 functools.wraps 来解决这个问题。
from functools import wraps
import time
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} 耗时: {time.time() - start:.4f}s")
return result
return wrapper
@timer
def process_data(items):
"""处理数据列表"""
return [x * 2 for x in items]
print(process_data.__name__) # 输出: process_data
print(process_data.__doc__) # 输出: 处理数据列表
@wraps(func) 会把原函数的元信息复制到 wrapper 函数上,包括 __name__、__doc__、__annotations__ 等,这是编写健壮装饰器的必备实践。
三、带参数的装饰器
有时候我们需要装饰器能够接收参数,比如指定重复次数、设置日志级别等。这需要再套一层函数结构。
from functools import wraps
def repeat(times):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
results = []
for _ in range(times):
results.append(func(*args, **kwargs))
return results[-1] # 返回最后一次结果
return wrapper
return decorator
@repeat(3)
def send_notification(message):
print(f"发送通知: {message}")
return True
send_notification("系统升级完成")
# 输出:
# 发送通知: 系统升级完成
# 发送通知: 系统升级完成
# 发送通知: 系统升级完成
带参数装饰器的结构是三层:最外层接收参数,中间层接收函数,最内层是实际的包装函数。虽然嵌套层次较多,但理解这个模式后就能写出灵活的装饰器。
四、类装饰器
除了函数装饰器,Python 还支持类装饰器。类装饰器通过实现 __call__ 方法,让类实例可以像函数一样被调用。
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
self.__name__ = func.__name__
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} 调用次数: {self.count}")
return self.func(*args, **kwargs)
@CountCalls
def fetch_data(url):
print(f"从 {url} 获取数据")
return {"status": "ok"}
fetch_data("api.example.com")
fetch_data("api.example.com")
# 输出:
# fetch_data 调用次数: 1
# 从 api.example.com 获取数据
# fetch_data 调用次数: 2
# 从 api.example.com 获取数据
类装饰器的好处是可以维护状态(如这里的 count 计数器),这在某些场景下比闭包更直观。但要注意实现 __name__ 等属性以保持良好的调试体验。
五、装饰器堆叠
多个装饰器可以堆叠使用,执行顺序是从下到上:离函数定义最近的装饰器最先执行。
from functools import wraps
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"调用 {func.__name__}")
return func(*args, **kwargs)
return wrapper
def validate_args(func):
@wraps(func)
def wrapper(*args, **kwargs):
if len(args) < 2:
raise ValueError(f"{func.__name__} 至少需要 2 个参数")
return func(*args, **kwargs)
return wrapper
@log_call
@validate_args
def divide(a, b):
return a / b
# 执行顺序: validate_args -> log_call -> divide
result = divide(10, 2)
# 输出: 调用 divide
print(result) # 输出: 5.0
装饰器堆叠可以实现职责分离:validate_args 负责参数验证,log_call 负责日志记录,divide 专注于业务逻辑。这让代码更清晰、更易维护。
六、实战案例:缓存装饰器
让我们实现一个实用的缓存装饰器,它能记住函数调用的结果,避免重复计算。
from functools import wraps
def memoize(func):
cache = {}
@wraps(func)
def wrapper(*args, **kwargs):
# 创建缓存的键
key = (args, frozenset(kwargs.items()))
if key not in cache:
cache[key] = func(*args, **kwargs)
print(f"计算 {func.__name__}({args}, {kwargs})")
else:
print(f"从缓存获取 {func.__name__}({args}, {kwargs})")
return cache[key]
# 添加清空缓存的方法
wrapper.clear_cache = lambda: cache.clear()
wrapper.cache_size = lambda: len(cache)
return wrapper
@memoize
def fibonacci(n):
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(10))
print(fibonacci(10)) # 从缓存获取
print(f"缓存大小: {fibonacci.cache_size()}")
fibonacci.clear_cache()
这个 memoize 装饰器展示了装饰器的强大之处:它可以为任何纯函数添加缓存能力,显著提升性能。对于 fibonacci 这样的递归函数,缓存能将时间复杂度从 O(2^n) 降到 O(n)。
七、装饰器的常见陷阱
使用装饰器时要注意几个常见问题:
1. 修改函数签名:装饰器可能改变函数的参数接口,导致类型检查工具失效。
2. 难以调试:过度使用装饰器会让调用栈变深,问题定位更困难。
3. 性能开销:每次函数调用都要经过包装器,对高性能敏感的场景要谨慎使用。
4. 可变参数问题:如 memoize 示例中,kwargs 需要转换为 frozenset 才能作为字典键。
八、总结
装饰器是 Python 中体现"优雅即正义"的绝佳例子。它让我们能够在不修改原函数代码的情况下,灵活地添加功能,体现了开放封闭原则。
从基础的时间计时装饰器,到带参数的重复执行装饰器,再到类装饰器和堆叠使用,我们看到了装饰器在不同场景下的应用。记住几个关键点:
• 使用 functools.wraps 保留函数元信息
• 带参数装饰器需要三层嵌套结构
• 类装饰器适合需要维护状态的场景
• 装饰器堆叠实现职责分离
• 缓存装饰器是性能优化的利器
掌握装饰器,你的代码将更具 Python 风格,更加优雅和强大。在实际项目中,善用装饰器可以显著提升代码的可读性和可维护性。
