问题与目标
计时、日志和重试会出现在许多函数周围。如果把这些逻辑复制进每个业务函数,代码很快失去一致性。闭包可以保存外围状态,装饰器可以在不改调用方式的前提下包装函数。
完成标准:能解释函数对象、闭包和装饰顺序,使用 functools.wraps 保留元数据,并实现一个只重试指定异常的装饰器。
核心概念
Python 函数可以赋值、传参和作为返回值。内部函数引用外层函数变量时,即使外层调用已经结束,这些变量仍可被闭包保存。
@decorator 等价于 function = decorator(function)。多个装饰器从下到上包装,调用时从上到下进入。包装器应使用 @wraps(func) 保留原函数名、文档和类型工具依赖的元数据。
闭包读取外层变量不需要声明;需要重新绑定时使用 nonlocal:
python
from collections.abc import Callable
def make_counter() -> Callable[[], int]:
count = 0
def increase() -> int:
nonlocal count
count += 1
return count
return increase
counter = make_counter()
print(counter(), counter())
每次调用保留同一份 count,但不同 make_counter() 调用产生彼此独立的状态。
重试只适合短暂故障,并且操作最好具备幂等性。参数错误、认证失败等确定性错误不应盲目重试。
可运行实现
python
from collections.abc import Callable
from functools import wraps
from time import sleep
from typing import ParamSpec, TypeVar
P = ParamSpec("P")
R = TypeVar("R")
def retry(attempts: int, delay: float = 0.0) -> Callable[[Callable[P, R]], Callable[P, R]]:
if attempts < 1:
raise ValueError("attempts 必须大于 0")
def decorate(func: Callable[P, R]) -> Callable[P, R]:
@wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
for current in range(1, attempts + 1):
try:
return func(*args, **kwargs)
except TimeoutError:
if current == attempts:
raise
print(f"第 {current} 次超时,准备重试")
sleep(delay)
raise RuntimeError("不可达分支")
return wrapper
return decorate
failures = 0
@retry(attempts=3, delay=0.1)
def fetch() -> str:
global failures
failures += 1
if failures < 3:
raise TimeoutError("temporary timeout")
return "ok"
print(fetch())
print(fetch.__name__)
输出两次重试提示、ok 和原函数名 fetch。
多个装饰器的顺序可以用最小实验确认:
python
def mark(name):
def decorate(func):
def wrapper():
print("enter", name)
result = func()
print("leave", name)
return result
return wrapper
return decorate
@mark("outer")
@mark("inner")
def work():
print("work")
work()
输出从 outer 进入、再进入 inner,退出顺序相反。生产装饰器仍应加入 wraps;这里省略它只为突出调用顺序。
常见问题与排查
- 装饰后函数名变成
wrapper:遗漏了functools.wraps。 - 捕获
Exception后无限重试:会掩盖编程错误;限定异常类型和最大次数。 - 被包装函数有写入副作用:重试可能重复扣款或重复插入,先设计幂等键或事务边界。
- 闭包中修改外层不可变变量:需要
nonlocal;共享可变状态还要考虑并发安全。 - 装饰器堆叠后行为难懂:保持装饰器单一职责,并为顺序写测试。
小结
闭包用于保存上下文,装饰器用于统一包裹函数边界。它们适合计时、日志、鉴权和有限重试,但不能让隐藏副作用的业务逻辑自动变可靠。
许可协议:CC BY-NC 4.0
更新于 1 小时前
觉得文章有帮助?点个赞吧!
0 条评论


