E.B.1 装饰器进阶用法


装饰器会在函数外面包一层可复用行为。当很多函数都需要同样的日志、计时、重试、权限检查或 trace 时,就适合用装饰器。
- Python 3.10+
- 不需要第三方包
- 理解函数基础
- Wrapper(包装函数):真正运行在原函数外层的内部函数。
- Cross-cutting logic(横切逻辑):很多地方都需要,但不属于业务核心的逻辑。
functools.wraps:装饰后仍保留原函数名称和元信息。- 装饰器顺序:函数被调用时,最上面的装饰器先执行。
运行日志和重试装饰器
Section titled “运行日志和重试装饰器”创建 decorator_demo.py:
from functools import wraps
def log_call(fn): @wraps(fn) def wrapper(*args, **kwargs): print(f"[LOG] start {fn.__name__}") result = fn(*args, **kwargs) print(f"[LOG] end {fn.__name__}") return result
return wrapper
def retry(max_retries=2): def decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): last_error = None for attempt in range(1, max_retries + 2): try: return fn(*args, **kwargs) except RuntimeError as error: last_error = error print(f"[RETRY] attempt={attempt} error={error}") raise last_error
return wrapper
return decorator
state = {"attempt": 0}
@log_call@retry(max_retries=2)def fetch_model_info(model_id): state["attempt"] += 1 if state["attempt"] < 2: raise RuntimeError("temporary network error") return {"model_id": model_id, "status": "ready"}
print(fetch_model_info("demo-v1"))print(fetch_model_info.__name__)运行:
python decorator_demo.py预期输出:
[LOG] start fetch_model_info[RETRY] attempt=1 error=temporary network error[LOG] end fetch_model_info{'model_id': 'demo-v1', 'status': 'ready'}fetch_model_info这个例子说明三件事:业务函数保持简短,重试逻辑集中管理,wraps 保留了函数名。
把装饰器顺序换成:
@retry(max_retries=2)@log_calldef fetch_model_info(model_id):这时日志会在每次重试内部执行。服务代码里装饰器顺序很重要。
什么时候适合用装饰器
Section titled “什么时候适合用装饰器”适合:
- 日志和追踪
- 计时
- 不稳定 I/O 的重试
- 权限检查
- 框架注册
如果包装层隐藏了关键业务逻辑,或者一个函数已经叠了太多层,就不适合继续加装饰器。
把装饰器放进真实代码前,先检查被包装函数是否仍然像原函数一样工作。输入不应该被偷偷改掉,返回值不应该变形,新增行为应该能在日志、计时输出或指标里看见。
一个好的装饰器留下证据,而不是隐藏控制流。如果调试更困难,就退回普通 helper 函数。目标不是语法漂亮,而是统一加入 trace、校验、重试或计时。
交付检查时,至少记录三件事:原函数名是否被 wraps 保留,异常是否按预期被重试或抛出,日志是否能看出开始和结束。只要其中一项看不见,装饰器就可能已经影响排障。
学完这一页,至少保留这张证据卡:
- Python 模式
- 装饰器、迭代器、生成器、并发原语,或元编程钩子
- 代码产物
- 最小可运行示例加上打印输出
- 使用场景
- 这种模式在哪种 AI 应用、流水线、工具或服务器中更有用
- 失败检查
- 隐藏副作用、难读的抽象、竞态条件或过度设计
- 期望产出
- 带实际 AI 系统用途说明的小型高级 Python 示例
- 忘记
@wraps,导致日志和框架看到的函数名都变成wrapper。 - 重试所有异常,包括本应立即失败的校验错误或权限错误。
- 装饰器堆太多,执行顺序很难排查。
在 fetch_model_info 前加一个 require_role("admin") 装饰器。非 admin 用户抛出 PermissionError,并且不要重试权限错误。
参考实现与讲解
好的实现会先处理权限,再让 retry 只处理临时失败。可以把 require_role("admin") 放在重试路径外层,或者修改 retry,让它遇到 PermissionError 时立即重新抛出。
预期行为是:
- admin 用户可以正常调用函数。
- 非 admin 用户会得到
PermissionError。 - 权限失败不会反复打印 retry 日志,因为权限错误不是临时网络错误。