>>>PyPathPython 学习站
首页›进阶篇›kp-024
进阶篇 · Advanced lv.3 进阶 kp-024

装饰器

前置知识:kp-007、kp-023

1. 一句话定义

装饰器是接收函数并返回新函数的可调用对象,@dec 语法糖等价于 f = dec(f)——在不改动原函数的前提下叠加横切能力(计时、缓存、鉴权、重试)。

2. 为什么重要

它是 Python 实现 AOP(面向切面)的第一工具:FastAPI 的路由注册、pytest 的 fixture、dataclass 的代码生成,全是装饰器。写不出带参装饰器,就谈不上"读懂现代 Python"。

3. 前置知识

kp-023(闭包)、kp-007(函数一等公民)。

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. 常见误区

  1. 忘写 functools.wraps —— 元数据丢失;这是装饰器编写的第一纪律。
  2. 把带参装饰器写成两层 —— @retry(5) 传进去的是 5,函数本身先被当参数处理;带参 = 三层。
  3. 在装饰器里做重活 —— 装饰发生在导入期,慢的初始化会拖累整个模块加载。
  4. wrapper 丢失签名导致类型检查/框架报错 —— 用 *args, **kwargs 转发是通用解,但也让静态工具看不到真实参数;关键公共 API 可考虑 functools.wraps + 显式签名。

10. 自测题

  1. @a @b def f() 等价于什么表达式?装饰顺序如何?
  2. 为什么 @retry(times=3) 需要三层嵌套?写出调用展开式。
  3. functools.wraps 解决什么问题?
参考答案
  1. f = a(b(f));先 b 后 a(自下而上包裹,自上而下书写)。
  2. f = retry(times=3)(f):第一次调用构建装饰器,第二次才包裹函数。
  3. 把被装饰函数的 __name__/__doc__/__wrapped__ 等元数据复制到 wrapper,保持自省一致。

11. 与其他知识点的关系

  • kp-025 上下文管理器:另一种"包裹执行"的语法设施,对照记忆。
  • kp-027 元编程:类装饰器与描述符。
  • kp-034 性能:装饰器链本身有调用开销。

12. 延伸阅读

  • PEP 318(Decorators)
  • functools.wraps 文档