Skip to content

03-装饰器核心原理

Python 版本要求:Python 3.11+

贯穿项目:Web API 请求处理系统
本节目标:理解 @ 语法的本质、functools.wraps 的作用、装饰器的执行时序

代码位置decorators_demo/app/decorators/ch03_basics.py
测试验证uv run pytest tests/test_ch03.py -v
API 演示uv run uvicorn app.main:app --reload → 访问 http://localhost:8000/docs


概念铺垫

1. 为什么需要装饰器?

问题场景:API 函数需要日志

继续 Web API 系统,每个处理函数都需要记录日志和耗时:

python
# ❌ 每个函数重复写日志和计时代码
def handle_users(req: Request) -> Response:
    print(f"[LOG] 调用 handle_users, 参数: {req}")   # 重复
    start = time.perf_counter()
    result = {"users": ["Alice", "Bob"]}
    elapsed = time.perf_counter() - start
    print(f"[LOG] handle_users 返回, 耗时: {elapsed:.3f}s")  # 重复
    return Response(200, result)

def handle_products(req: Request) -> Response:
    print(f"[LOG] 调用 handle_products, 参数: {req}")  # 重复
    start = time.perf_counter()
    result = {"products": ["Apple"]}
    elapsed = time.perf_counter() - start
    print(f"[LOG] handle_products 返回, 耗时: {elapsed:.3f}s")  # 重复
    return Response(200, result)

# ✅ 用装饰器统一添加
@log_call
@timer
def handle_users(req: Request) -> Response:
    return Response(200, {"users": ["Alice", "Bob"]})

@log_call
@timer
def handle_products(req: Request) -> Response:
    return Response(200, {"products": ["Apple"]})

核心问题:如何不修改原函数代码,就能统一添加功能?

2. 生活类比:礼物包装

把装饰器想象成礼物包装纸:

┌─────────────────────────────────────────────────────────────┐
│  装饰器 = 礼物包装纸                                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  原函数 = 礼物                                                │
│  • 有核心功能(如 handle_users 处理请求)                    │
│  • 不改变内在                                                │
│                                                             │
│  装饰器 = 包装纸                                              │
│  • 包裹礼物(包装原函数)                                     │
│  • 增加外观功能(日志、计时)                                 │
│  • 不改变礼物本身                                            │
│                                                             │
│  wrapper = 包装后的礼物                                       │
│  • 外面是包装纸(装饰器添加的功能)                           │
│  • 里面是礼物(原函数)                                       │
│  • 拿起来还是礼物,但多了包装                                 │
│                                                             │
│  示例:                                                      │
│  ─────────────────────                                      │
│  @log_call                    # 外层包装纸                   │
│  @timer                       # 内层包装纸                   │
│  def handle_users(req):       # 礼物                        │
│      return process(req)                                     │
│                                                             │
│  调用 handle_users(req) 时:                                 │
│  • 先执行外层包装纸(log_call 前置)                          │
│  • 再执行内层包装纸(timer 开始计时)                         │
│  • 执行礼物功能(处理请求)                                   │
│  • 内层包装纸继续(timer 结束计时)                           │
│  • 外层包装纸继续(log_call 后置)                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘

一句话:

装饰器 = 不修改原函数,动态添加功能的包装函数


L1 理解层:会用

3. @ 语法的本质

@decorator 只是一个语法糖,本质就是函数替换。

等价写法

┌─────────────────────────────────────────────────────────────┐
│  @语法 vs 手动包装                                             │
│                                                             │
│  方式1:@语法(推荐)                                          │
│  ─────────────────────────────                               │
│  @log_call                                                   │
│  def handle_users(req):                                      │
│      return process(req)                                     │
│                                                             │
│  方式2:手动包装(完全等价)                                   │
│  ─────────────────────────────                               │
│  def handle_users(req):                                      │
│      return process(req)                                     │
│  handle_users = log_call(handle_users)                      │
│                                                             │
│  ⚡ 执行过程:                                                 │
│  1. Python 定义原函数 handle_users                           │
│  2. 调用 log_call(handle_users)                              │
│  3. log_call 返回 wrapper                                    │
│  4. 将 handle_users 重新绑定到 wrapper                       │
│                                                             │
│  后续调用 handle_users(req) 时:                              │
│  实际调用的是 wrapper(req)                                    │
│  wrapper 内部再调用原函数                                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘

4. 最简装饰器示例

只需要 3 行代码就能理解装饰器的本质:

python
from functools import wraps
from typing import Callable, TypeVar

T = TypeVar("T")

def simple_decorator(func: Callable[..., T]) -> Callable[..., T]:
    """最简单的装饰器 — 什么都不做,只演示原理"""

    @wraps(func)
    def wrapper(*args, **kwargs) -> T:
        return func(*args, **kwargs)

    return wrapper

分解说明:

部分作用
def simple_decorator(func)接收一个函数作为参数
def wrapper(*args, **kwargs)定义包装函数,接收任意参数
return func(*args, **kwargs)调用原函数并返回结果
return wrapper返回包装函数替代原函数
@wraps(func)保留原函数的元信息

完整代码见app/decorators/ch03_basics.py

5. functools.wraps 到底复制了哪些属性

@wraps(func) 会把原函数的元信息复制到 wrapper 上。对比使用前后的差异:

python
from functools import wraps
from typing import Callable, TypeVar

T = TypeVar("T")

# ❌ 不用 @wraps — 丢失元信息
def bad_decorator(func: Callable[..., T]) -> Callable[..., T]:
    def wrapper(*args, **kwargs) -> T:
        return func(*args, **kwargs)
    return wrapper

# ✅ 用 @wraps — 保留元信息
def good_decorator(func: Callable[..., T]) -> Callable[..., T]:
    @wraps(func)
    def wrapper(*args, **kwargs) -> T:
        return func(*args, **kwargs)
    return wrapper

def my_func() -> str:
    """这是原函数的文档字符串"""
    return "result"

# 对比
bad_version = bad_decorator(my_func)
good_version = good_decorator(my_func)

print(bad_version.__name__)   # "wrapper"   ← 错误!
print(bad_version.__doc__)    # None         ← 丢失!
print(good_version.__name__)  # "my_func"    ← 正确
print(good_version.__doc__)   # "这是..."    ← 保留
print(good_version.__wrapped__)  # <function my_func>  ← 可访问原函数

wraps 复制的属性清单

属性说明
__name__函数名
__doc__文档字符串
__module__所属模块
__qualname__限定名
__annotations__类型注解
__dict__自定义属性
__wrapped__指向原函数(由 wraps 添加)

实际影响:不用 @wraps,IDE 无法给出正确的类型提示和文档,调试时看到的函数名也是 wrapper

互动演示:启动服务后访问 /api/v1/demo/wraps-comparison 查看实时对比。

6. 装饰器执行时间线

装饰器有两个执行阶段:定义阶段(模块加载时)和 调用阶段(函数被调用时)。

┌─────────────────────────────────────────────────────────────┐
│  装饰器执行时间线                                              │
│                                                             │
│  代码:                                                       │
│  @log_call                                                   │
│  def handle_users(req):                                      │
│      return process(req)                                     │
│                                                             │
│  handle_users(request)                                       │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │ 时间线                                               │   │
│  │                                                      │   │
│  │  1. 定义阶段(加载模块时,只执行一次)               │   │
│  │     • Python 定义 handle_users                       │   │
│  │     • 调用 log_call(handle_users)                    │   │
│  │     • log_call 返回 wrapper                          │   │
│  │     • handle_users = wrapper                        │   │
│  │        │                                             │   │
│  │        ↓                                             │   │
│  │  2. 调用阶段(每次调用都执行)                       │   │
│  │     handle_users(request)                            │   │
│  │     实际执行 wrapper(request)                        │   │
│  │        │                                             │   │
│  │        ↓                                             │   │
│  │  3. wrapper 执行前置逻辑                             │   │
│  │     记录日志: "调用 handle_users"                    │   │
│  │        │                                             │   │
│  │        ↓                                             │   │
│  │  4. 调用原函数                                       │   │
│  │     result = func(request)                           │   │
│  │     • func 是被保存在闭包中的原 handle_users        │   │
│  │        │                                             │   │
│  │        ↓                                             │   │
│  │  5. wrapper 执行后置逻辑                             │   │
│  │     记录日志: "handle_users 返回"                    │   │
│  │     return result                                     │   │
│  │                                                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ⚠️ 关键:定义时就完成包装,调用时执行 wrapper                  │
│  ⚠️ 关键:原函数 func 被保存在闭包中                           │
│  ⚠️ 关键:装饰器在定义阶段执行,不是调用阶段                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

7. 多层装饰器的执行顺序

多个装饰器叠加时有两个方向:从下往上包装(定义阶段),从上往下执行(调用阶段)。

python
@deco_a
@deco_b
def target():
    print("target")

# 等价于
target = deco_a(deco_b(target))

执行顺序图解

定义阶段(从下往上包装):
───────────────────────────
target                    ← 原始函数


deco_b(target)            ← 先包装内层


wrapper_b                 ← 得到内层包装


deco_a(wrapper_b)         ← 再包装外层


wrapper_a                 ← 最终绑定到 target

调用阶段(从上往下执行):
───────────────────────────
target()


wrapper_a before          ← 外层前置


wrapper_b before          ← 内层前置


target body               ← 原函数


wrapper_b after           ← 内层后置


wrapper_a after           ← 外层后置

代码验证

python
call_order = []

def deco_a(func):
    def wrapper(*args, **kwargs):
        call_order.append("A-before")
        result = func(*args, **kwargs)
        call_order.append("A-after")
        return result
    return wrapper

def deco_b(func):
    def wrapper(*args, **kwargs):
        call_order.append("B-before")
        result = func(*args, **kwargs)
        call_order.append("B-after")
        return result
    return wrapper

@deco_a
@deco_b
def target():
    call_order.append("target")

target()
print(call_order)
# 输出: ['A-before', 'B-before', 'target', 'B-after', 'A-after']

完整测试:见 tests/test_ch03.py::test_stacked_decorators

8. 常见坑

坑 1:不用 @wraps 导致 IDE 无法提示

python
# ❌ 不用 @wraps
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@bad_decorator
def add(a: int, b: int) -> int:
    return a + b

add(1, 2)          # 返回值类型变成 Any,IDE 无法推断
add.__name__       # "wrapper" 而不是 "add"
add.__doc__        # None

解决:始终使用 @wraps(func)

坑 2:装饰器忘记 return 原函数结果

python
# ❌ 忘记 return
def broken_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("before")
        func(*args, **kwargs)  # 没有 return!
        print("after")
    return wrapper

@broken_decorator
def get_value() -> int:
    return 42

result = get_value()
print(result)  # None!不是 42

# ✅ 正确写法
def good_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("before")
        result = func(*args, **kwargs)
        print("after")
        return result  # ← 必须 return
    return wrapper

坑 3:多层装饰器顺序导致的 bug

python
# @timer 需要访问 _last_elapsed 属性
@log_call
@timer
def process(data):
    return data

# ✅ 正确:timer 在内层,_last_elapsed 挂在 process 上
process("hello")
print(process._last_elapsed)  # 可以访问

# ❌ 如果反过来:
@timer
@log_call
def process2(data):
    return data

# timer 的 wrapper 挂在最外层,_last_elapsed 挂在 timer 的 wrapper 上
# 但 log_call 的 wrapper 在中间层,访问 process2._last_elapsed 会找不到

规则:需要暴露属性的装饰器应放在最内层(最靠近原函数)。

9. 贯穿实战:日志与计时装饰器

本章的实现包含两个实用装饰器:

@log_call — 日志装饰器

python
_call_log: list[dict[str, Any]] = []

def log_call(func: Callable[..., T]) -> Callable[..., T]:
    """日志装饰器 — 记录函数调用信息"""

    @wraps(func)
    def wrapper(*args, **kwargs) -> T:
        _call_log.append({
            "func": func.__name__,
            "args": args,
            "kwargs": kwargs,
            "status": "calling",
        })
        result = func(*args, **kwargs)
        _call_log.append({
            "func": func.__name__,
            "result": result,
            "status": "returned",
        })
        return result

    return wrapper

特性

  • 使用全局 _call_log 列表记录调用信息
  • 每次调用记录两条日志:调用前 + 返回后
  • 提供 get_call_log()clear_call_log() 测试辅助函数

@timer — 计时装饰器

python
def timer(func: Callable[..., T]) -> Callable[..., T]:
    """计时装饰器 — 记录函数执行耗时"""

    @wraps(func)
    def wrapper(*args, **kwargs) -> T:
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        wrapper._last_elapsed = elapsed
        return result

    wrapper._last_elapsed = 0.0
    return wrapper

特性

  • 使用 time.perf_counter() 精确计时
  • 将耗时保存在 wrapper._last_elapsed 属性上供外部查询
  • 初始化 _last_elapsed = 0.0 避免首次查询报错

API 路由演示

python
# app/routers/demo.py
@log_call
@timer
def _process_data(data: str) -> dict[str, str]:
    """模拟数据处理 — 被两个装饰器包装"""
    return {"processed": data, "status": "ok"}

@router.get("/hello")
def demo_hello(name: str = "World") -> dict[str, str]:
    result = _process_data(name)
    return {
        **result,
        "elapsed_ms": f"{_process_data._last_elapsed * 1000:.2f}",
    }

互动演示

bash
cd decorators_demo
uv run uvicorn app.main:app --reload
# 访问 http://localhost:8000/api/v1/demo/hello?name=Python

完整代码app/decorators/ch03_basics.pyapp/routers/demo.py


L2 实践层:用好

推荐做法

做法原因示例
始终使用 @wraps(func)保留函数名、文档、类型注解@wraps(func) 在 wrapper 前
wrapper 用 *args, **kwargs装饰器可用于任意签名的函数def wrapper(*args, **kwargs):
wrapper 必须 return func() 的结果否则原函数返回值丢失result = func(*args, **kwargs); return result
time.perf_counter() 计时单调时钟,不受系统时间调整影响from time import perf_counter
需要暴露属性的装饰器放最内层属性可被外部直接访问@log_call @timer def f(): timer 在内层
通过 __wrapped__ 访问原函数调试、测试时绕开装饰器func.__wrapped__(*args)

反模式:不要这样做

python
# ❌ 错误:装饰器不用 @wraps
def bad_decorator(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper
# ✅ 正确:
def good_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

# ❌ 错误:wrapper 忘记 return
def broken_timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        func(*args, **kwargs)  # 没有 return!
        print(f"耗时: {time.perf_counter() - start:.3f}s")
    return wrapper

# ❌ 错误:装饰器修改了传入参数但不声明(破坏原函数契约)
def force_lowercase(func):
    @wraps(func)
    def wrapper(text, *args, **kwargs):
        return func(text.lower(), *args, **kwargs)  # 静默修改参数
    return wrapper
# ✅ 正确:如果需要修改参数,在文档中明确说明

# ❌ 错误:在装饰器层面做耗时操作(每次定义都执行)
def slow_decorator(func):
    time.sleep(1)  # 定义阶段就被执行,拖慢启动
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

适用场景

场景是否推荐原因
日志记录✅ 推荐装饰器最适合横切关注点,不改原函数
性能计时✅ 推荐拆解计时逻辑,原函数专注业务
权限验证✅ 推荐统一鉴权逻辑,避免每个函数重复
缓存✅ 推荐@lru_cache 一行代码完成
重试机制✅ 推荐重试逻辑与业务逻辑完全解耦
修改函数返回值类型❌ 不推荐破坏类型安全,调用方预期混乱
修改函数参数⚠️ 慎重必须在文档中明确说明,避免隐式副作用
性能敏感的热路径⚠️ 慎重每次调用多一层函数栈帧,开销 ~50ns

L3 专家层:深入

Python 如何实现:装饰器语法糖的降级过程

@decorator 在 Python 编译时被**降级(desugar)**为显式赋值:

┌─────────────────────────────────────────────────────────────┐
│  @decorator 语法的编译过程                                     │
│                                                             │
│  源代码:                                                     │
│  @log_call                                                   │
│  def handle_users(req):                                      │
│      return process(req)                                     │
│                                                             │
│  Python 编译器(compile 阶段)将其转换为:                       │
│  ─────────────────────────────────────                       │
│  def handle_users(req):                                      │
│      return process(req)                                     │
│  handle_users = log_call(handle_users)                      │
│                                                             │
│  字节码验证(dis.dis 查看):                                   │
│  ─────────────────────────────                               │
│  import dis                                                 │
│  code = """                                                  │
│  @log_call                                                   │
│  def handle_users(req):                                      │
│      pass                                                    │
│  """                                                         │
│  dis.dis(compile(code, '', 'exec'))                          │
│                                                             │
│  关键字节码:                                                  │
│  LOAD_NAME      'log_call'  ← 加载装饰器                     │
│  LOAD_CONST     <handle_users>  ← 加载函数对象               │
│  CALL           1  ← 调用 log_call(handle_users)            │
│  STORE_NAME     'handle_users'  ← 赋值给变量名               │
│                                                             │
└─────────────────────────────────────────────────────────────┘

CPython 源码中的实现:在 Python/compile.c 中,编译器遇到 @ 表达式时生成 PRECALL + CALL + STORE_NAME 三条指令序列。这不是运行时行为,而是编译时的语法转换。

多层装饰器 @a @b def f(): 的降级:

python
# 等价于
f = a(b(f))  # 从内向外:b 先包装,a 再包装

functools.wraps 的内部实现

wraps 本身是一个装饰器,但它不是包装函数调用——它只在定义阶段把原始函数的属性复制到 wrapper 上:

python
# functools.wraps 的简化版实现(CPython 源码简化)
def wraps(wrapped,
          assigned=WRAPPER_ASSIGNMENTS,   # 默认: __module__,__name__,__qualname__,
                                         #       __annotations__,__doc__
          updated=WRAPPER_UPDATES):       # 默认: __dict__
    """类似 functools.update_wrapper 的装饰器版本"""
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

验证:wraps 复制的具体属性及来源:

python
import functools

def original():
    """原函数的文档"""
    pass

@functools.wraps(original)
def wrapper():
    pass

# 检查复制了哪些属性
print(functools.WRAPPER_ASSIGNMENTS)
# ('__module__', '__name__', '__qualname__', '__annotations__', '__doc__')
print(functools.WRAPPER_UPDATES)
# ('__dict__',)

# wraps 添加的特殊属性
print(wrapper.__wrapped__ is original)  # True(wraps 显式设置)

__wrapped__ 属性的作用

wraps 除了复制属性,还设置 wrapper.__wrapped__ = wrapped。这个属性是多层装饰器调试的关键:

python
@deco_a
@deco_b
def target():
    pass

# 沿着 __wrapped__ 链遍历
target.__wrapped__           # → deco_b 的 wrapper
target.__wrapped__.__wrapped__  # → 原始 target

# 获取最终原始函数
def unwrap(func):
    while hasattr(func, '__wrapped__'):
        func = func.__wrapped__
    return func

unwrapped = unwrap(target)
print(unwrapped)  # <function target at ...>

性能考量

操作时间复杂度说明
@decorator 定义阶段O(1)每个装饰器在模块加载时执行 1 次
wrapper() 每次调用O(1)比直接调用多一层栈帧,开销 ~50ns
多层装饰器(n 层)O(n) per call每层多一次函数调用 + 前置/后置逻辑
@wraps(func)O(n) at define timen = 复制属性个数,仅定义时执行
unwrap() 遍历O(m)m = 装饰器层数
场景开销说明
无装饰器直接调用~40ns基准
1 层简单装饰器~90ns+50ns(wrapper 调用 + args 解包)
5 层装饰器~300ns+260ns(5 层前置 + 5 层后置)
热路径上建议< 3 层超过 3 层考虑合并装饰器或非装饰器方案

知识关联

┌─────────────────────────────────────────────────────────────┐
│  知识关联图:装饰器核心 → 高级用法                              │
│                                                             │
│  第 3 章:装饰器核心原理                                       │
│  ┌──────────────────────────────────────┐                   │
│  │ • @decorator = func = decorator(func)│                   │
│  │ • @wraps 保留元信息                  │                   │
│  │ • 定义阶段 vs 调用阶段               │                   │
│  │ • 多层装饰器执行顺序                  │                   │
│  └──────────┬───────────────────────────┘                   │
│             │                                                │
│    ┌────────┼───────────────┬──────────────┐                 │
│    ↓        ↓               ↓              ↓                 │
│  第4章    第5章            第6章          第7章               │
│  带参数    标准装饰器       ParamSpec     边界情况             │
│  装饰器    lru_cache       类装饰器       __wrapped__链       │
│           singledispatch   async装饰器   异常栈               │
│                                                             │
│  Python 标准库中的装饰器例子:                                  │
│  ───────────────────────────                                │
│  • @staticmethod, @classmethod, @property                   │
│  • @contextlib.contextmanager                               │
│  • @dataclasses.dataclass                                   │
│  • @functools.lru_cache                                     │
│  • @atexit.register                                         │
│                                                             │
│  设计模式映射:                                                │
│  • 装饰器 = Proxy 模式(控制访问 + 添加行为)                   │
│  • 装饰器 = Decorator 模式(GoF,Python 直接语法支持)          │
│  • 不使用装饰器 = AOP(面向切面编程)的替代方案                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

10. 自检清单

回答以下问题,检查你是否掌握了核心概念:

  1. @decorator 等价于什么手动写法?
  2. @wraps(func) 保留了哪些属性?至少说出 3 个
  3. 装饰器在哪个阶段执行?定义阶段还是调用阶段?
  4. 多层装饰器 @a @b def f(): pass 的执行顺序是什么?
  5. 不用 @wraps 会带来哪些实际问题?

答案

  1. func = decorator(func) — 将装饰器返回值重新绑定到原函数名
  2. __name__(函数名)、__doc__(文档字符串)、__wrapped__(指向原函数)、__annotations__(类型注解)
  3. 装饰器本身在定义阶段执行(模块加载时),wrapper 在调用阶段执行
  4. 定义时从下往上:f = a(b(f));调用时从外往里:a_wrapper → b_wrapper → f
  5. IDE 无法推断类型和显示文档、__name__ 变成 "wrapper"、调试困难、help() 显示错误信息

11. 本章能力清单

学完本章,你能够:

  • [x] 理解 @decoratorfunc = decorator(func) 的语法糖
  • [x] 编写最简装饰器(wrapper 调用原函数并返回结果)
  • [x] 使用 @wraps(func) 保留函数元信息
  • [x] 编写日志装饰器(记录调用参数和返回值)
  • [x] 编写计时装饰器(记录执行耗时)
  • [x] 理解多层装饰器的执行顺序(从下往上包装,从上往下执行)
  • [x] 避免常见坑:忘记 @wraps、忘记 return、装饰器顺序错误
  • [x] 通过 API 路由交互式探索装饰器效果