进阶篇 · Advanced
lv.3 进阶
kp-024
装饰器
1. 一句话定义
装饰器是接收函数并返回新函数的可调用对象,@dec 语法糖等价于 f = dec(f)——在不改动原函数的前提下叠加横切能力(计时、缓存、鉴权、重试)。
2. 为什么重要
它是 Python 实现 AOP(面向切面)的第一工具:FastAPI 的路由注册、pytest 的 fixture、dataclass 的代码生成,全是装饰器。写不出带参装饰器,就谈不上"读懂现代 Python"。
3. 前置知识
4. 核心概念
python
import functools, time
# ① 基础装饰器
def timed(fn):
@functools.wraps(fn) # 保住原函数的 __name__/__doc__
def wrapper(*args, **kwargs):
t0 = time.perf_counter()
try:
return fn(*args, **kwargs)
finally:
print(f"{fn.__name__} 耗时 {time.perf_counter()-t0:.3f}s")
return wrapper
@timed
def work(n): time.sleep(n)
# ② 带参装饰器 = 三层函数(工厂返回装饰器)
def retry(times=3, delay=0.5):
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
for i in range(times):
try:
return fn(*args, **kwargs)
except Exception:
if i == times - 1: raise
time.sleep(delay)
return wrapper
return decorator
@retry(times=5, delay=1.0)
def fetch(): ...
# 等价:fetch = retry(times=5, delay=1.0)(fetch)- 装饰器栈:
@a在上、@b在下 →a(b(f)),自下而上包裹。 - 装饰器不限函数:装饰类(
@dataclass)、类方法(@classmethod)皆可。
5. 原理与机制
@x 在定义时执行一次:把紧随其后的名字作为参数传给 x,结果替换原名。所以装饰器发生的时机是导入期,运行期只剩被替换后的 wrapper。
functools.wraps 复制原函数元数据到 wrapper——没有它,work.__name__ 会变成 "wrapper",调试、文档、框架自省全部失真。
6. 关键事实(模型/图示)
text
@retry(times=5)
def fetch(): ...
① retry(times=5) → 返回 decorator(此时只构建装饰器)
② decorator(fetch) → 返回 wrapper(此时完成包裹)
③ 名字 fetch → 绑定到 wrapper
调用链:fetch(args) → wrapper(args) → 原函数(args)7. 直观类比
装饰器像快递的层层包裹:@b 先给函数套一层箱子(自下而上),@a 再套一层。调用时从最外层拆起(wrapper 链),每层都能在"拆到内层之前/之后"做自己的事(前置/后置逻辑)。
8. 实例与案例
python
# 注册器模式:路由表 / 插件发现
ROUTES = {}
def route(path):
def decorator(fn):
ROUTES[path] = fn
return fn # 原样返回:只登记,不包裹
return decorator
@route("/hello")
def hello(): return "hi"这是框架层装饰器的另一半用法:不包裹、只登记。
9. 常见误区
- 忘写
functools.wraps—— 元数据丢失;这是装饰器编写的第一纪律。 - 把带参装饰器写成两层 ——
@retry(5)传进去的是 5,函数本身先被当参数处理;带参 = 三层。 - 在装饰器里做重活 —— 装饰发生在导入期,慢的初始化会拖累整个模块加载。
- wrapper 丢失签名导致类型检查/框架报错 —— 用
*args, **kwargs转发是通用解,但也让静态工具看不到真实参数;关键公共 API 可考虑functools.wraps+ 显式签名。
10. 自测题
@a @b def f()等价于什么表达式?装饰顺序如何?- 为什么
@retry(times=3)需要三层嵌套?写出调用展开式。 functools.wraps解决什么问题?
参考答案
f = a(b(f));先 b 后 a(自下而上包裹,自上而下书写)。f = retry(times=3)(f):第一次调用构建装饰器,第二次才包裹函数。- 把被装饰函数的
__name__/__doc__/__wrapped__等元数据复制到 wrapper,保持自省一致。
11. 与其他知识点的关系
12. 延伸阅读
- PEP 318(Decorators)
- functools.wraps 文档