文章目录
-
- 一、从一个重复问题开始
- 二、装饰器的最小形态
- [三、`@` 语法糖:等价变换](#三、
@语法糖:等价变换) - 四、`functools.wraps`:为什么不能省
-
- [`functools.wraps` 的底层机制](#
functools.wraps的底层机制)
- [`functools.wraps` 的底层机制](#
- 五、多层装饰器的执行顺序
- 六、装饰器在执行上下文中的位置
- 七、装饰器参数检查的实战技巧
- 八、`inspect.signature`:让装饰器感知签名
- [九、类方法的装饰器:`self` 的注意事项](#九、类方法的装饰器:
self的注意事项) - 十、基于装饰器的注册机制
- 十一、总结:装饰器的本质与边界
一、从一个重复问题开始
假设有这样的需求:记录每个函数的执行时间。
最直接的写法:
python
import time
def fetch_user(user_id):
start = time.perf_counter()
# 真正的逻辑
result = {"id": user_id, "name": "Alice"}
end = time.perf_counter()
print(f"fetch_user 耗时 {end - start:.4f}s")
return result
def fetch_order(order_id):
start = time.perf_counter()
result = {"id": order_id, "amount": 99.9}
end = time.perf_counter()
print(f"fetch_order 耗时 {end - start:.4f}s")
return result
计时代码在每个函数里重复出现------如果有 50 个函数,就得粘贴 50 次。更糟的是,计时逻辑和业务逻辑混在一起,后者变得难以测试。
这就是装饰器要解决的问题:把横切关注点(计时、日志、认证等)从业务逻辑中剥离出来,独立维护。
二、装饰器的最小形态
在理解装饰器之前,先从一个最小的例子开始------一个不做任何修改的"透传"装饰器:
python
def do_nothing(fn):
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
def greet(name):
return f"Hello, {name}"
decorated = do_nothing(greet)
print(decorated("Alice")) # Hello, Alice
这就是装饰器的核心结构:
do_nothing接收一个函数fn(被装饰的函数)- 内部定义
wrapper,用*args, **kwargs接收任意参数,转发给原函数 - 返回
wrapper
decorated 的类型是函数,是 wrapper,但它的行为和 greet 完全相同。
现在加上计时逻辑:
python
import time
def timer(fn):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{fn.__name__} 耗时 {elapsed:.4f}s")
return result
return wrapper
def fetch_user(user_id):
return {"id": user_id, "name": "Alice"}
fetch_user = timer(fetch_user) # 手动装饰
fetch_user(42) # fetch_user 耗时 0.0000s
fetch_user = timer(fetch_user) 这一行做了三件事:
- 把原来的
fetch_user函数传给timer timer返回了一个包含计时逻辑的wrapperwrapper重新绑定到fetch_user这个名字上
此后所有对 fetch_user 的调用,实际上调用的都是 wrapper。
三、@ 语法糖:等价变换
Python 提供了 @ 语法,让装饰器的写法更简洁:
python
@timer
def fetch_user(user_id):
return {"id": user_id, "name": "Alice"}
这完全等价于:
python
def fetch_user(user_id):
return {"id": user_id, "name": "Alice"}
fetch_user = timer(fetch_user)
@timer 只是语法糖,Python 解释器在处理 def fetch_user(...) 的同时,自动执行了 fetch_user = timer(fetch_user)。
验证这两种写法是等价的:
python
import dis
# 写法一
source_a = """
@timer
def f():
pass
"""
# 写法二
source_b = """
def f():
pass
f = timer(f)
"""
# 编译后的字节码会包含完全相同的 CALL 指令
@ 语法的价值在于:装饰器声明紧跟函数定义,阅读代码时一眼就能看到函数被哪些装饰器处理过。分开写时,函数定义和装饰操作可能相隔很远,容易遗漏。
四、functools.wraps:为什么不能省
现在有一个问题。使用 timer 装饰 fetch_user 后:
python
@timer
def fetch_user(user_id):
"""根据 user_id 获取用户信息"""
return {"id": user_id, "name": "Alice"}
print(fetch_user.__name__) # wrapper(而不是 fetch_user)
print(fetch_user.__doc__) # None(而不是函数的文档字符串)
fetch_user 变成了 wrapper------名字、文档字符串、注解全都丢失了。这在生产代码中会引发严重问题:日志里的函数名全是 wrapper;help(fetch_user) 显示的是错误的文档;pytest 在测试报告中无法正确标记失败的函数。
functools.wraps 解决这个问题:
python
import functools
import time
def timer(fn):
@functools.wraps(fn) # 把 fn 的元信息复制到 wrapper 上
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = fn(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{fn.__name__} 耗时 {elapsed:.4f}s")
return result
return wrapper
@timer
def fetch_user(user_id):
"""根据 user_id 获取用户信息"""
return {"id": user_id, "name": "Alice"}
print(fetch_user.__name__) # fetch_user ✓
print(fetch_user.__doc__) # 根据 user_id 获取用户信息 ✓
functools.wraps 的底层机制
@functools.wraps(fn) 本身也是一个装饰器------装饰器用来装饰装饰器内部的 wrapper。它等价于:
python
wrapper = functools.update_wrapper(wrapper, fn)
update_wrapper 把以下属性从 fn 复制到 wrapper:
| 属性 | 内容 |
|---|---|
__name__ |
函数名 |
__qualname__ |
完全限定名(包含类名等) |
__doc__ |
文档字符串 |
__dict__ |
函数的自定义属性字典(浅拷贝) |
__annotations__ |
类型注解 |
__module__ |
所在模块名 |
__wrapped__ |
指向原始函数的指针(新增,用于解包) |
最后一个 __wrapped__ 特别实用:
python
@timer
def calculate(x, y):
return x + y
# 获取原始函数(绕过装饰器)
original = calculate.__wrapped__
print(original(1, 2)) # 3,不打印计时信息
在单元测试中,__wrapped__ 让测试代码直接调用原始函数,不经过装饰器逻辑,保证测试的纯粹性。
五、多层装饰器的执行顺序
当多个装饰器叠加时,执行顺序遵循一个固定规则。先看代码:
python
def decorator_a(fn):
print(f"装饰器 A 包装 {fn.__name__}")
@functools.wraps(fn)
def wrapper(*args, **kwargs):
print(f"A: 调用 {fn.__name__} 之前")
result = fn(*args, **kwargs)
print(f"A: 调用 {fn.__name__} 之后")
return result
return wrapper
def decorator_b(fn):
print(f"装饰器 B 包装 {fn.__name__}")
@functools.wraps(fn)
def wrapper(*args, **kwargs):
print(f"B: 调用 {fn.__name__} 之前")
result = fn(*args, **kwargs)
print(f"B: 调用 {fn.__name__} 之后")
return result
return wrapper
@decorator_a
@decorator_b
def target():
print("target 执行")
输出(定义阶段):
装饰器 B 包装 target
装饰器 A 包装 target
输出(调用 target() 时):
A: 调用 target 之前
B: 调用 target 之前
target 执行
B: 调用 target 之后
A: 调用 target 之后
规律:
- 包装阶段(定义时):从下到上执行,先 B 再 A
- 调用阶段(运行时):从外到内进入(A 先、B 后),从内到外返回(B 先、A 后)
理解这个规律,关键是把 @decorator_a @decorator_b def target() 展开:
python
# 等价代码
target = decorator_a(decorator_b(target))
# ^--- 先执行 decorator_b(target)
# ^--- 再执行 decorator_a(...)
decorator_b 先包装 target,返回 wrapper_b;decorator_a 再包装 wrapper_b,返回 wrapper_a。调用 target() 时,实际调用的是 wrapper_a,它内部调用 wrapper_b,wrapper_b 才调用原始 target。
调用 target()
进入 decorator_a 的 wrapper
A: 调用前
进入 decorator_b 的 wrapper
B: 调用前
执行原始 target()
B: 调用后
返回 decorator_b 的 wrapper
A: 调用后
返回 decorator_a 的 wrapper
返回最终结果
六、装饰器在执行上下文中的位置
装饰器的定义 和调用发生在不同的时间点,这是理解装饰器行为的关键:
python
print("模块加载开始")
def logger(fn):
print(f" [装饰] {fn.__name__}") # 模块加载时执行
@functools.wraps(fn)
def wrapper(*args, **kwargs):
print(f" [调用] {fn.__name__}") # 每次调用时执行
return fn(*args, **kwargs)
return wrapper
print("定义装饰函数...")
@logger
def api_get():
return "data"
@logger
def api_post():
return "created"
print("模块加载结束")
print("开始调用...")
api_get()
api_post()
输出:
模块加载开始
定义装饰函数...
[装饰] api_get
[装饰] api_post
模块加载结束
开始调用...
[调用] api_get
[调用] api_post
装饰器的包装逻辑([装饰] 部分)在模块被 import 时就已经执行完毕,而不是等到函数被调用时。这个特性在以下场景中需要格外注意:
- 注册机制 :框架(如 Flask 的路由
@app.route)利用这个特性在导入模块时自动注册路由 - 副作用:如果装饰器的包装逻辑有副作用(如连接数据库),模块导入时就会触发
七、装饰器参数检查的实战技巧
很多装饰器需要对参数做验证,比如确保某个参数是正整数:
python
import functools
from typing import Callable, Any
def validate_positive(fn: Callable) -> Callable:
"""确保第一个位置参数是正数"""
@functools.wraps(fn)
def wrapper(*args, **kwargs) -> Any:
if args:
first_arg = args[0]
if isinstance(first_arg, (int, float)) and first_arg <= 0:
raise ValueError(
f"{fn.__name__} 的第一个参数必须是正数,收到了 {first_arg!r}"
)
return fn(*args, **kwargs)
return wrapper
@validate_positive
def calculate_discount(price, rate=0.1):
return price * (1 - rate)
@validate_positive
def generate_range(n):
return list(range(n))
# 正常情况
print(calculate_discount(100)) # 90.0
# 传入负数时
try:
calculate_discount(-50)
except ValueError as e:
print(e) # calculate_discount 的第一个参数必须是正数,收到了 -50
这个装饰器通过 args[0] 访问第一个位置参数,避免了在每个函数里手写参数校验逻辑。
八、inspect.signature:让装饰器感知签名
上面的 validate_positive 只能验证位置参数,无法处理关键字传参的情况(calculate_discount(price=-50))。inspect.signature 可以解决这个问题:
python
import inspect
import functools
def validate_positive_v2(fn):
sig = inspect.signature(fn)
@functools.wraps(fn)
def wrapper(*args, **kwargs):
# 把所有参数绑定到签名
bound = sig.bind(*args, **kwargs)
bound.apply_defaults() # 补上默认值
# 遍历所有参数
for name, value in bound.arguments.items():
if isinstance(value, (int, float)) and value <= 0:
raise ValueError(
f"{fn.__name__}() 的参数 {name!r} 必须是正数,收到了 {value!r}"
)
return fn(*args, **kwargs)
return wrapper
@validate_positive_v2
def calculate_bill(price, tax_rate=0.1):
return price * (1 + tax_rate)
try:
calculate_bill(price=100, tax_rate=-0.5)
except ValueError as e:
print(e)
# calculate_bill() 的参数 'tax_rate' 必须是正数,收到了 -0.5
inspect.signature(fn).bind(*args, **kwargs) 把实际传入的参数绑定到函数签名上,返回的 BoundArguments 可以统一以字典形式访问所有参数------无论是位置传参还是关键字传参,都能处理。
九、类方法的装饰器:self 的注意事项
把装饰器用在类方法上时,self(实例引用)会作为第一个参数传入 wrapper:
python
import functools
import time
def method_timer(fn):
"""专门用于类方法的计时装饰器"""
@functools.wraps(fn)
def wrapper(self, *args, **kwargs):
start = time.perf_counter()
result = fn(self, *args, **kwargs)
elapsed = time.perf_counter() - start
cls_name = type(self).__name__
print(f"{cls_name}.{fn.__name__} 耗时 {elapsed:.4f}s")
return result
return wrapper
class DataPipeline:
def __init__(self, name):
self.name = name
@method_timer
def process(self, data):
"""处理数据"""
result = [x ** 2 for x in data]
return result
@method_timer
def aggregate(self, processed_data):
"""聚合数据"""
return sum(processed_data)
pipeline = DataPipeline("test")
processed = pipeline.process(range(1000))
total = pipeline.aggregate(processed)
# DataPipeline.process 耗时 0.0001s
# DataPipeline.aggregate 耗时 0.0000s
通用装饰器(用 *args 接收所有参数)也能直接用在类方法上,因为 self 会被收进 args[0]。但显式写出 wrapper(self, *args, **kwargs) 更清晰,也能在 wrapper 内部访问实例属性。
十、基于装饰器的注册机制
装饰器另一个重要用途是自动注册。框架(Flask、Click)大量使用这个模式:
python
class EventBus:
"""一个简单的事件总线"""
_handlers = {}
@classmethod
def on(cls, event_name):
"""将函数注册为某事件的处理器"""
def decorator(fn):
if event_name not in cls._handlers:
cls._handlers[event_name] = []
cls._handlers[event_name].append(fn)
return fn # 注意:注册类装饰器通常返回原函数,不修改行为
return decorator
@classmethod
def emit(cls, event_name, *args, **kwargs):
"""触发事件,调用所有注册的处理器"""
handlers = cls._handlers.get(event_name, [])
results = []
for handler in handlers:
results.append(handler(*args, **kwargs))
return results
# 注册事件处理器
@EventBus.on("user.created")
def send_welcome_email(user_id, email):
return f"发送欢迎邮件到 {email}"
@EventBus.on("user.created")
def create_default_profile(user_id, email):
return f"创建用户 {user_id} 的默认配置"
@EventBus.on("order.placed")
def notify_inventory(order_id):
return f"通知库存系统:订单 {order_id}"
# 触发事件
results = EventBus.emit("user.created", user_id=1, email="alice@example.com")
print(results)
# ['发送欢迎邮件到 alice@example.com', '创建用户 1 的默认配置']
这个模式的核心是:装饰器在函数定义时 就把函数注册到了 _handlers 字典里,之后通过事件名就能找到所有处理器并调用。Flask 的 @app.route("/api/users") 本质上就是这样工作的。
十一、总结:装饰器的本质与边界
装饰器的本质
高阶函数:接收函数,返回函数
闭包:wrapper 捕获 fn 作为自由变量
@语法:f = decorator(f) 的语法糖
两个关键规范
functools.wraps:保留元信息
*args/**kwargs:兼容任意签名
多层装饰器
包装顺序:从下到上
调用顺序:从外到内
应用场景
横切关注点分离
(计时/日志/认证)
注册机制
(路由/事件/命令)
参数校验
(类型/范围/权限)
装饰器是 Python 里最能体现"函数是一等公民"这一特性的语言机制。它把两个函数对象(装饰器函数和被装饰函数)通过闭包绑定在一起,使得通用逻辑能以最低的代码侵入性附着在业务函数上。
理解装饰器的关键认知路径:
- 函数是对象,可以作为参数和返回值 → #01
- 闭包能捕获外层变量,让 wrapper 持有 fn 的引用 → #02
@语法是f = decorator(f)的等价写法 → 本文functools.wraps是规范写法,不能省略 → 本文- 带参数的装饰器、类装饰器 → #04
如果觉得本文对深入理解 Python 有帮助,欢迎点赞和收藏。本专栏专注 Python 工程化与底层机制,每一篇都有配套代码和原理剖析,关注不迷路。