Python 装饰器高级用法:带参数、类装饰器与缓存
Python 装饰器高级用法:带参数、类装饰器与缓存
装饰器是 Python 中一种强大的语法糖,它允许你在不修改原函数代码的前提下,为函数添加额外的功能。掌握了基础装饰器的写法后,理解带参数的装饰器、类装饰器以及缓存装饰器,将帮助你写出更灵活、可复用的代码。本教程面向有一定基础但想深入装饰器用法的开发者,逐步拆解三种高级模式。
1. 回顾:最简装饰器
一个最基础的装饰器本质上是一个接受函数、返回新函数的高阶函数:
def simple_decorator(func):
def wrapper(*args, **kwargs):
print(f"调用 {func.__name__} 之前")
result = func(*args, **kwargs)
print(f"调用 {func.__name__} 之后")
return result
return wrapper
@simple_decorator
def greet(name):
print(f"你好,{name}")
greet("世界")
输出:
调用 greet 之前
你好,世界
调用 greet 之后
如果我们需要让装饰器本身接收参数,比如控制是否启用日志,就不能简单套用上面的模式了。
2. 带参数的装饰器 —— 再包装一层
带参数的装饰器本质上是一个返回装饰器的函数。我们必须在装饰器外面再定义一层函数,用来接收装饰器参数。
2.1 语法结构
def decorator_with_args(arg1, arg2):
def actual_decorator(func):
def wrapper(*args, **kwargs):
# 可以使用 arg1, arg2
print(f"参数: {arg1}, {arg2}")
return func(*args, **kwargs)
return wrapper
return actual_decorator
使用时,@decorator_with_args(value1, value2) 会先调用 decorator_with_args(...),返回 actual_decorator,然后再用 actual_decorator 装饰目标函数。
2.2 实际案例:日志级别控制
让我们实现一个可以根据 level 参数决定日志行为的装饰器。
def log(level="INFO"):
def decorator(func):
def wrapper(*args, **kwargs):
if level == "DEBUG":
print(f"[DEBUG] 调用 {func.__name__},参数: {args}, {kwargs}")
else:
print(f"[{level}] 执行 {func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
@log(level="DEBUG")
def add(a, b):
return a + b
@log(level="WARNING")
def subtract(a, b):
return a - b
add(3, 5)
subtract(10, 4)
输出:
[DEBUG] 调用 add,参数: (3, 5), {}
[WARNING] 执行 subtract
2.3 使用 functools.wraps 保留元信息
多层包装会丢失原函数的 __name__、__doc__ 等属性。一定要用 functools.wraps 将内层 wrapper 的函数信息更新为原函数的。
修正后的版本:
from functools import wraps
def log(level="INFO"):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"[{level}] {func.__name__}")
return func(*args, **kwargs)
return wrapper
return decorator
wraps 是一个装饰器,它自身接收原函数作为参数,并完成元信息复制。在带参数的装饰器中,它应该直接装饰内层的 wrapper。
3. 类装饰器 —— 用对象管理状态
装饰器不仅可以是函数,任何可调用对象都可以作为装饰器。类实现 __call__ 方法即可充当装饰器。类装饰器最大的优势是能够方便地维护状态,并且逻辑可以拆分成多个方法,结构更清晰。
3.1 基本类装饰器
class CountCalls:
def __init__(self, func):
self.func = func
self.num_calls = 0
def __call__(self, *args, **kwargs):
self.num_calls += 1
print(f"第 {self.num_calls} 次调用 {self.func.__name__}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello():
print("Hello!")
say_hello()
say_hello()
输出:
第 1 次调用 say_hello
Hello!
第 2 次调用 say_hello
Hello!
3.2 带参数的类装饰器
要让类装饰器接收参数,只需改写 __init__ 方法,让它接收参数,并返回一个可调用对象(实例本身)作为实际的装饰器。装饰过程的调用顺序与函数装饰器类似。
class CountCallsWithLimit:
def __init__(self, max_calls):
self.max_calls = max_calls
self.call_count = 0
def __call__(self, func):
self.func = func
return self.wrapper_function
def wrapper_function(self, *args, **kwargs):
self.call_count += 1
if self.call_count > self.max_calls:
raise RuntimeError("调用次数超限!")
print(f"调用 {self.func.__name__},次数 {self.call_count}/{self.max_calls}")
return self.func(*args, **kwargs)
使用:
@CountCallsWithLimit(3)
def limited_hello():
print("Hello")
limited_hello()
limited_hello()
limited_hello()
# limited_hello() # 这里会抛出 RuntimeError
注意:为了正确保留原函数元信息,我们仍可在 wrapper_function 上使用 @wraps(func)(需要在 __call__ 内手动包裹)。
3.3 在类装饰器中正确保留原函数信息
可在 __call__ 内部对 wrapper 应用 wraps:
from functools import wraps
class Retry:
def __init__(self, max_retries=3):
self.max_retries = max_retries
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, self.max_retries + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"尝试 {attempt} 失败: {e}")
raise Exception(f"{self.max_retries} 次重试后仍失败")
return wrapper
@Retry(max_retries=2)
def unstable_func():
print("执行中...")
raise ValueError("连接失败")
unstable_func()
4. 实战:用装饰器实现缓存
缓存是装饰器的一个经典应用场景。我们可以将函数的输入参数和对应的返回值存储起来,下次遇到相同参数时直接返回缓存结果,避免重复计算。Python 标准库提供的 functools.lru_cache 就是现成的强大工具,但自己实现一个可以更深入地理解装饰器的工作方式。
4.1 简单字典缓存装饰器
def simple_cache(func):
cache = {}
@wraps(func)
def wrapper(*args):
if args in cache:
print(f"缓存命中: {args}")
return cache[args]
result = func(*args)
cache[args] = result
return result
return wrapper
@simple_cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10)) # 第一次计算
print(fibonacci(10)) # 缓存命中
局限:该装饰器仅支持以位置参数作为缓存键,且不能处理关键字参数和不可哈希类型。生产环境中我们应使用 functools.lru_cache 或自定义更健壮的键生成机制。
4.2 利用 functools.lru_cache
from functools import lru_cache
@lru_cache(maxsize=128)
def compute_heavy(x, y):
print(f"实际计算: {x} + {y}")
return x + y
compute_heavy(3, 5) # 实际计算
compute_heavy(3, 5) # 不触发实际计算
lru_cache 支持:
maxsize:缓存最大条目数,设置为None时无限缓存。typed:是否区分不同类型的相同值(如3和3.0)。- 自动处理关键字参数并维护 LRU 淘汰策略。
4.3 支持过期时间的缓存装饰器
某些场景需要缓存具备时效性。我们可以用类装饰器结合时间戳实现。
import time
from functools import wraps
class TTL_Cache:
def __init__(self, ttl_seconds=60):
self.ttl = ttl_seconds
self.storage = {} # key -> (timestamp, result)
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
# 生成稳定的键(支持关键字参数)
key = (args, tuple(sorted(kwargs.items())))
if key in self.storage:
ts, value = self.storage[key]
if time.time() - ts < self.ttl:
return value
result = func(*args, **kwargs)
self.storage[key] = (time.time(), result)
return result
return wrapper
@TTL_Cache(5)
def get_data(source):
print(f"从 {source} 获取数据...")
return f"数据来自 {source}"
print(get_data("API")) # 从 API 获取数据...
time.sleep(2)
print(get_data("API")) # 返回缓存
time.sleep(4)
print(get_data("API")) # 超过5秒,重新获取
5. 组合技巧与最佳实践
5.1 装饰器的执行顺序
当函数被多个装饰器装饰时,执行的顺序是从下往上(靠近函数定义的装饰器先执行),但调用时是从上往下。
@decorator_a
@decorator_b
def func():
pass
等价于 func = decorator_a(decorator_b(func))。
5.2 类装饰器 vs. 函数装饰器选择
- 需要维护复杂状态或提供额外属性、方法时,优先使用类装饰器。
- 功能简单、无需状态的装饰器,使用函数加
wraps更为简洁。 - 带参数的装饰器用函数实现也很直观,类装饰器在参数复杂时结构更清晰。
5.3 装饰器通用编写模板
无参数函数装饰器:
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 前置处理
res = func(*args, **kwargs)
# 后置处理
return res
return wrapper
带参数函数装饰器:
def decorator(param1, param2):
def actual_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# 可使用 param1, param2
return func(*args, **kwargs)
return wrapper
return actual_decorator
带参数的类装饰器:
class DecoratorWithArgs:
def __init__(self, param):
self.param = param
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
# 可使用 self.param
return func(*args, **kwargs)
return wrapper
5.4 调试与元信息保护
无论哪种装饰器,只要内部定义了新函数,务必使用 @wraps(func) 来继承原函数的 __name__、__doc__、__module__ 等属性,否则会影响调试和文档自动生成。在类装饰器中,如果返回的是实例自身的可调用方法,请在该方法上应用 wraps。
6. 小结与扩展
至此,你已经掌握了装饰器的三种进阶形态:
- 带参数的装饰器:通过嵌套一层函数来接收参数,实现更灵活的行为控制。
- 类装饰器:利用
__call__方法将类实例变为可调用装饰器,便于管理状态和逻辑拆分。 - 缓存装饰器:从自制的简单字典缓存到标准库
lru_cache,再到带过期时间的自定义实现,极大提升了重复计算的性能。
装饰器还可以应用于类方法、类本身,甚至与描述器结合使用。如果你想进一步探索,可以尝试实现一个权限校验装饰器,或者一个自动重试的 TCP 连接装饰器。记住,理解装饰器本质(将函数/类作为参数并返回可调用对象)比死记硬背语法更重要。多动手实践,你很快就能设计出优雅且强大的装饰器。