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


装饰器会在函数外面包一层可复用行为。当很多函数都需要同样的日志、计时、重试、权限检查或 trace 时,就适合用装饰器。
准备内容
- Python 3.10+
- 不需要第三方包
- 理解函数基础
关键术语
- Wrapper(包装函数):真正运行在原函数外层的内部函数。
- Cross-cutting logic(横切逻辑):很多地方都需要,但不属于业务核心的逻辑。
functools.wraps:装饰后仍保留原函数名称和元信息。- 装饰器顺序:函数被调用时,最上面的装饰器先执行。
运行日志和重试装饰器
创建 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_call
def fetch_model_info(model_id):
这时日志会在每次重试内部执行。服务代码里装饰器顺序很重要。
什么时候适合用装饰器
适合:
- 日志和追踪
- 计时
- 不稳定 I/O 的重试
- 权限检查
- 框架注册
如果包装层隐藏了关键业务逻辑,或者一个函数已经叠了太多层,就不适合继续加装饰器。
常见错误
- 忘记
@wraps,导致日志和框架看到的函数名都变成wrapper。 - 重试所有异常,包括本应立即失败的校验错误或权限错误。
- 装饰器堆太多,执行顺序很难排查。
练习
在 fetch_model_info 前加一个 require_role("admin") 装饰器。非 admin 用户抛出 PermissionError,并且不要重试权限错误。