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 系统,每个处理函数都需要记录日志和耗时:
# ❌ 每个函数重复写日志和计时代码
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 行代码就能理解装饰器的本质:
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 上。对比使用前后的差异:
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. 多层装饰器的执行顺序
多个装饰器叠加时有两个方向:从下往上包装(定义阶段),从上往下执行(调用阶段)。
@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 ← 外层后置代码验证
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 无法提示
# ❌ 不用 @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 原函数结果
# ❌ 忘记 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
# @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 — 日志装饰器
_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 — 计时装饰器
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 路由演示
# 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}",
}互动演示:
cd decorators_demo
uv run uvicorn app.main:app --reload
# 访问 http://localhost:8000/api/v1/demo/hello?name=Python完整代码:app/decorators/ch03_basics.py、app/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) |
反模式:不要这样做
# ❌ 错误:装饰器不用 @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(): 的降级:
# 等价于
f = a(b(f)) # 从内向外:b 先包装,a 再包装functools.wraps 的内部实现
wraps 本身是一个装饰器,但它不是包装函数调用——它只在定义阶段把原始函数的属性复制到 wrapper 上:
# 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 复制的具体属性及来源:
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。这个属性是多层装饰器调试的关键:
@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 time | n = 复制属性个数,仅定义时执行 |
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. 自检清单
回答以下问题,检查你是否掌握了核心概念:
@decorator等价于什么手动写法?@wraps(func)保留了哪些属性?至少说出 3 个- 装饰器在哪个阶段执行?定义阶段还是调用阶段?
- 多层装饰器
@a @b def f(): pass的执行顺序是什么? - 不用
@wraps会带来哪些实际问题?
答案:
func = decorator(func)— 将装饰器返回值重新绑定到原函数名__name__(函数名)、__doc__(文档字符串)、__wrapped__(指向原函数)、__annotations__(类型注解)- 装饰器本身在定义阶段执行(模块加载时),wrapper 在调用阶段执行
- 定义时从下往上:
f = a(b(f));调用时从外往里:a_wrapper → b_wrapper → f - IDE 无法推断类型和显示文档、
__name__变成"wrapper"、调试困难、help()显示错误信息
11. 本章能力清单
学完本章,你能够:
- [x] 理解
@decorator是func = decorator(func)的语法糖 - [x] 编写最简装饰器(wrapper 调用原函数并返回结果)
- [x] 使用
@wraps(func)保留函数元信息 - [x] 编写日志装饰器(记录调用参数和返回值)
- [x] 编写计时装饰器(记录执行耗时)
- [x] 理解多层装饰器的执行顺序(从下往上包装,从上往下执行)
- [x] 避免常见坑:忘记
@wraps、忘记return、装饰器顺序错误 - [x] 通过 API 路由交互式探索装饰器效果