Python 装饰器与包装器
装饰器接收一个对象,并返回装饰后名称所指向的对象;返回的可以是原对象,不一定是新对象。函数装饰器常返回一个包装函数,由它调用原函数。下面的 traced 就是这样:每次调用包装函数时,先打印原函数名称,再转交实参,最后返回原函数的结果。这里的类型注解描述了参数与结果的传递关系:P 代表原函数的参数,R 代表返回类型,Callable[P, R] 表示具有这些参数和返回类型的可调用对象。
from collections.abc import Callable
from functools import wraps
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def traced(function: Callable[P, R]) -> Callable[P, R]:
@wraps(function)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"calling {function.__qualname__}")
return function(*args, **kwargs)
return wrapper
这里使用 functools.wraps。它负责保留原函数的元数据(如 __name__, __doc__)并暴露 __wrapped__ 属性。这对文档生成、调试器以及依赖元数据的其他装饰器至关重要。同时,通过 ParamSpec 和 TypeVar,我们能在静态类型检查中保留被包装函数的原始签名。
求值时机与组合顺序
装饰器表达式在定义语句执行时立即求值,通常发生在模块导入阶段。因此,装饰器内部的注册逻辑或配置操作应让副作用可预测。
当多个装饰器堆叠时,执行顺序是从内向外(即从下往上):
@outer
@inner
def operation(): ...
# 等价于:operation = outer(inner(operation))
这意味着顺序直接影响行为。例如,在缓存、重试、鉴权、事务或日志场景中,@cache 放在 @retry 外面还是里面,结果可能截然不同。
带参数的装饰器(工厂模式)
如果装饰器需要配置参数,它实际上是一个“工厂函数”,返回真正的装饰器:
class TransientError(Exception):
pass
def retry(*, attempts: int):
if attempts < 1:
raise ValueError("attempts must be positive")
def decorate(function):
@wraps(function)
def wrapper(*args, **kwargs):
for attempt in range(attempts):
try:
return function(*args, **kwargs)
except TransientError:
if attempt == attempts - 1:
raise
return wrapper
return decorate
重试策略需要明确哪些异常可恢复,以及重复执行是否安全。通用装饰器无法自动推断这些业务规则。
设计边界与陷阱
- 同步与异步不匹配:用同步包装器包裹异步函数,返回的是协程对象而非执行结果,调用者必须
await。生成器也有类似的生命周期管理问题。 - 不要隐藏复杂逻辑:避免将关键的控制流、I/O 操作或宽泛的异常吞没隐藏在看似简单的装饰器语法背后。这会降低代码的可读性和可维护性。
lru_cache的适用性:它要求参数可哈希,且仅在“复用结果在语义上是安全的”场景下适用。如果函数依赖可变的外部状态或时间敏感的结果,使用缓存会导致错误。- 并非所有装饰器都是包装器:
property、classmethod、staticmethod、contextmanager和singledispatch虽然使用装饰器语法,但定义了完全不同的契约。不要试图用“包装函数”的思维去理解它们,应分别掌握各自的 API 语义。
调用装饰后的函数
带类型注解的示例使用 ParamSpec,需要 Python 3.10+。定义 traced 后可以运行:
@traced
def add(left: int, right: int = 1) -> int:
return left + right
assert add(2, right=3) == 5
assert add.__name__ == "add"
assert add.__wrapped__(2, 3) == 5
只有第一次调用会打印 calling add。*args 和 **kwargs 分别转发位置参数和关键字参数;返回被包装函数的结果,才能保留调用者期待的返回值。wraps 不会强制检查类型,也不会把同步包装器变成异步包装器。
重试示例中的 TransientError 是演示用的异常,实际使用时应换成操作可能抛出的、可恢复的具体异常。对生成器函数重试,只会重试创建生成器,不会捕获随后迭代时的异常;异步操作的异常同样在等待执行时才出现。退避和抖动适用于存在竞争的远程服务,不是每次本地重试的必要条件。首先要判断部分完成的操作能否安全重复。