Python装饰器详解
装饰器(Decorator)是Python中一个非常强大且有用的特性,它允许我们在不修改原函数代码的情况下,为函数添加额外的功能。下面我将详细解释装饰器的使用和运行过程。
一、装饰器基础概念
装饰器本质上是一个Python函数(或其他可调用对象),它可以让其他函数在不做任何代码变动的情况下增加额外功能(增加新功能的函数偷偷换掉被装饰的函数)。装饰器的返回值也是一个函数对象(返回的对象就是那个增加新功能的函数)。
二、装饰器的运行过程
装饰器的运行过程分为两个阶段,装饰阶段和调用阶段
1. 装饰阶段(Definition Time)模块加载时执行
当Python解释器遇到 @decorator 语法时,会立即执行装饰器逻辑(在函数定义时执行,而非调用时)。
装饰器函数返回一个函数(增加新功能的函数)
将新函数赋值给被装饰器修饰的函数(原函数)
2. 调用阶段(Call Time)显式调用时执行
当调用被装饰的函数时,实际执行的是装饰器函数返回的函数(函数名字虽然一样 但是函数内容已经是装饰器修饰刚改过的了)。
当 Python 解释器遇到 @decorator
语法时,会立即执行装饰器逻辑(在函数/类定义阶段执行),这一行为是装饰器的核心特性。下面通过分步拆解和示例详细说明:
关键概念解析
定义阶段(Definition Time)
- 当 Python 加载模块或执行代码块时,遇到
@decorator
会立即执行装饰器函数。 - 此时不会执行被装饰的函数,而是将函数/类作为参数传递给装饰器。
- 当 Python 加载模块或执行代码块时,遇到
调用阶段(Call Time)
- 当通过函数名显式调用被装饰的函数时,才会执行装饰器返回的包装逻辑和原函数逻辑。
执行流程分步说明
场景代码
def decorator(func):
print("装饰器执行:修改函数行为") # 此代码在定义阶段立即执行
def wrapper(*args, **kwargs):
print("调用阶段:装饰器添加的逻辑")
return func(*args, **kwargs)
return wrapper
@decorator # 解释器遇到此行时立即执行 decorator(example)
def example():
print("原函数逻辑")
print("------ 分割线 ------")
example() # 第一次调用
example() # 第二次调用
输出结果
装饰器执行:修改函数行为 # 定义阶段输出
------ 分割线 ------ # 模块加载完成后
调用阶段:装饰器添加的逻辑 # 第一次调用输出
原函数逻辑
调用阶段:装饰器添加的逻辑 # 第二次调用输出
原函数逻辑
具体执行步骤
定义阶段(模块加载时)
- Python 解释器遇到
@decorator
时,会立即调用decorator(example)
,其中example
是尚未执行的原始函数对象。 - 执行
decorator
函数体:- 打印
"装饰器执行:修改函数行为"
- 定义
wrapper
函数(但不会执行它) - 返回
wrapper
函数
- 打印
- 关键操作:将
example
变量重新绑定到wrapper
函数(原函数可通过example.__wrapped__
访问)。
- Python 解释器遇到
调用阶段(显式调用时)
- 当代码执行
example()
时:- 实际调用的是
wrapper(*args, **kwargs)
- 执行
wrapper
内部的逻辑(如打印"调用阶段:装饰器添加的逻辑"
) - 通过
func(*args, **kwargs)
调用原始example
函数
- 实际调用的是
- 当代码执行
类比说明
阶段 | 类比场景 | 发生时机 |
---|---|---|
定义阶段 | 给手机安装滤镜APP(@decorator) | 安装APP时(模块加载/代码执行) |
调用阶段 | 用滤镜APP拍照(调用被装饰函数) | 按下快门时(显式函数调用) |
常见误区澄清
误区:认为装饰器逻辑在函数调用时才执行。
正解:装饰器本身(即decorator(func)
的调用)在定义阶段立即执行,返回的wrapper
逻辑在调用阶段执行。误区:每次调用被装饰函数都会重新执行装饰器。
正解:装饰器仅在定义阶段执行一次,后续调用直接使用已生成的wrapper
。
验证实验
通过修改示例观察执行顺序:
print("模块开始加载")
def decorator(func):
print("装饰器运行时间点") # 观察何时打印
return func # 直接返回原函数(无包装)
@decorator
def test():
pass
print("模块加载完成")
test() # 调用函数
输出:
模块开始加载
装饰器运行时间点 # 在模块加载过程中执行
模块加载完成
(若装饰器在调用时执行,此处的 装饰器运行时间点
会在 test()
调用后打印)
三、使用示例
以下是 Python 中各类装饰器的 定义说明、参数/返回值详解 和 具体使用示例:
一、函数装饰器(无参数)
定义说明
- 作用:修改或增强函数行为
- 入参:
func
(被装饰的函数对象) - 返回值:
wrapper
函数(替换原函数)
示例
def log_time(func):
def wrapper(*args, **kwargs):
import time
start = time.time()
result = func(*args, **kwargs) # 调用原函数
print(f"耗时: {time.time() - start:.2f}s")
return result
return wrapper
@log_time # 相当于 func = log_time(func)
def calculate(n):
return sum(i*i for i in range(n))
print(calculate(1000000)) # 调用时自动计时
二、带参数的函数装饰器
定义说明
- 作用:根据参数动态生成装饰器
- 入参:装饰器参数 → 返回实际装饰器 → 接收
func
- 返回值:多层嵌套的
wrapper
函数
示例
def repeat(times): # 装饰器工厂
def decorator(func): # 实际装饰器
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3) # 传递参数
def greet(name):
print(f"Hello, {name}!")
greet("Alice") # 打印3次问候语
三、类装饰器(装饰函数)
定义说明
- 作用:用类实现装饰器逻辑
- 入参:
func
通过__init__
接收 - 返回值:实现
__call__
方法使实例可调用
示例
class CountCalls:
def __init__(self, func):
self.func = func
self.calls = 0
def __call__(self, *args, **kwargs):
self.calls += 1
print(f"调用次数: {self.calls}")
return self.func(*args, **kwargs)
@CountCalls # 相当于 func = CountCalls(func)
def say_hello():
print("Hello!")
say_hello() # 每次调用统计次数
say_hello()
四、类装饰器(装饰类)
定义说明
- 作用:动态修改类定义
- 入参:
cls
(被装饰的类) - 返回值:修改后的类
示例
def add_method(cls):
def decorator(func):
setattr(cls, func.__name__, func)
return func
return decorator
@add_method # 装饰类
class MyClass:
pass
# 动态添加类方法
@MyClass.add_method
def new_method(self):
print("动态添加的方法")
obj = MyClass()
obj.new_method() # 调用新增方法
五、保留元信息的装饰器
定义说明
- 作用:避免装饰器掩盖原函数的
__name__
、__doc__
等属性 - 关键:使用
functools.wraps
复制元信息
示例
from functools import wraps
def preserve_meta(func):
@wraps(func) # 将原函数的元信息复制到wrapper
def wrapper(*args, **kwargs):
"""Wrapper函数的docstring"""
return func(*args, **kwargs)
return wrapper
@preserve_meta
def original():
"""原始函数的docstring"""
pass
print(original.__name__) # 输出 "original"(而非wrapper)
print(original.__doc__) # 输出 "原始函数的docstring"
六、方法装饰器(装饰类方法)
定义说明
- 作用:装饰类中的实例方法
- 注意:
wrapper
需接收self
参数
示例
def validate_input(func):
def wrapper(self, x):
if not isinstance(x, int):
raise ValueError("输入必须是整数")
return func(self, x)
return wrapper
class Calculator:
@validate_input
def square(self, x):
return x * x
calc = Calculator()
print(calc.square(5)) # 正常调用
calc.square("abc") # 触发验证错误
七、异步函数装饰器
定义说明
- 作用:装饰协程函数(
async def
) - 关键:
wrapper
需定义为async
并await
原函数
示例
def async_retry(max_attempts):
def decorator(func):
async def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts+1):
try:
return await func(*args, **kwargs)
except Exception as e:
print(f"尝试 {attempt} 失败: {e}")
raise RuntimeError("所有重试均失败")
return wrapper
return decorator
@async_retry(max_attempts=3)
async def fetch_data():
# 模拟可能失败的异步操作
raise ConnectionError("网络错误")
import asyncio
asyncio.run(fetch_data())
八、属性装饰器(@property
)
定义说明
- 作用:将方法转为属性调用
- 相关装饰器:
@property
,@x.setter
,@x.deleter
示例
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Getter: 获取半径"""
return self._radius
@radius.setter
def radius(self, value):
"""Setter: 设置半径"""
if value <= 0:
raise ValueError("半径必须为正数")
self._radius = value
circle = Circle(5)
print(circle.radius) # 调用getter(无需括号)
circle.radius = 10 # 调用setter
总结对比表
类型 | 定义形式 | 核心参数 | 返回值要求 | 典型场景 |
---|---|---|---|---|
函数装饰器 | def decorator(func): |
func |
wrapper 函数 |
日志、计时、验证 |
带参装饰器 | def decorator(params): → 嵌套 |
参数 + func |
多层 wrapper |
动态配置装饰行为 |
类装饰器(函数) | class Decorator: + __call__ |
func |
可调用实例 | 状态保持(如计数器) |
类装饰器(类) | def decorator(cls): |
cls |
修改后的类 | 动态添加类成员 |
异步装饰器 | async def wrapper: + await func() |
协程函数 | 异步包装函数 | 重试、异步日志 |
属性装饰器 | @property + @x.setter |
无 | 描述符对象 | 属性访问控制 |
通过组合这些模式,可以实现复杂的装饰器逻辑,同时保持代码的可读性。
以下是针对每一类装饰器的 实际应用场景 和 使用建议,结合具体需求说明其适用性和最佳实践:
四、应用场景及示例
一、函数装饰器(无参数)
应用场景
- 性能监控:记录函数执行时间(如 API 接口耗时)
- 调试工具:打印函数输入/输出(调试复杂逻辑时)
- 权限校验:检查用户权限(如
@login_required
)
建议
- 适用于 无状态 的简单增强逻辑
- 优先使用
functools.wraps
保留元信息 - 示例扩展:数据库事务自动提交
def db_transaction(func): @wraps(func) def wrapper(*args, **kwargs): try: result = func(*args, **kwargs) db.commit() # 成功则提交 return result except Exception as e: db.rollback() # 失败回滚 raise e return wrapper
- 示例扩展:函数执行时间计算
import time
from functools import wraps
# 定义装饰器:计算函数执行时间
def log_time(func):
@wraps(func) # 保留原函数元信息
def wrapper(*args, **kwargs):
start = time.time() # 记录开始时间
result = func(*args, **kwargs) # 调用原函数
end = time.time() # 记录结束时间
print(f"{func.__name__} 耗时: {end - start:.4f}秒") # 打印耗时
return result # 返回原函数结果
return wrapper # 返回包装后的函数
# 使用装饰器
@log_time
def calculate_sum(n):
"""计算1到n的平方和"""
return sum(i*i for i in range(n))
# 测试
print(calculate_sum(1000000)) # 输出结果和耗时
执行输出:
calculate_sum 耗时: 0.0453秒
333332833333500000
二、带参数的函数装饰器
应用场景
- 重试机制:根据参数控制重试次数和间隔(如网络请求)
- 缓存控制:动态设置缓存过期时间(如
@cache(ttl=300)
) - 权限分级:通过参数指定所需权限等级
建议
- 需要 动态配置 时使用
- 避免嵌套过深(超过 3 层可考虑用类装饰器替代)
- 示例扩展:指数退避重试
def retry(max_attempts, delay=1): def decorator(func): @wraps(func) def wrapper(*args, **kwargs): for attempt in range(1, max_attempts+1): try: return func(*args, **kwargs) except Exception: if attempt == max_attempts: raise time.sleep(delay * 2 ** attempt) # 指数退避 return wrapper return decorator
三、类装饰器(装饰函数)
应用场景
- 状态维护:统计函数调用次数/频率
- 资源管理:自动管理文件/锁的开关(如
with
语句的替代) - 单例模式:强制一个类只有一个实例
建议
- 需要 长期保持状态 时使用
- 示例扩展:函数调用限流
class RateLimiter: def __init__(self, func, max_calls=10): self.func = func self.max_calls = max_calls self.calls = 0 def __call__(self, *args, **kwargs): if self.calls >= self.max_calls: raise RuntimeError("超过调用限额") self.calls += 1 return self.func(*args, **kwargs)
四、类装饰器(装饰类)
应用场景
- 动态注入:为类添加通用方法(如
to_dict()
) - 接口适配:统一实现某些接口(如序列化协议)
- 注册机制:自动注册子类(如插件系统)
建议
- 适用于 框架开发 或 元编程
- 示例扩展:自动注册子类
registered_plugins = {} def register_plugin(name): def decorator(cls): registered_plugins[name] = cls return cls return decorator @register_plugin("csv_reader") class CSVReader: pass
五、保留元信息的装饰器
应用场景
- 文档生成:确保
help()
和__doc__
正确显示 - 调试工具:保留原始函数名便于日志追踪
- AOP 框架:面向切面编程时保持堆栈信息
建议
- 始终 对函数装饰器使用
@wraps
- 调试时可通过
__wrapped__
访问原函数
六、方法装饰器(装饰类方法)
应用场景
- 输入验证:校验方法参数类型/范围
- 单位转换:自动转换参数单位(如角度 ↔ 弧度)
- 上下文管理:自动加锁/释放资源
建议
- 注意
self
是类实例,可访问类属性 - 示例扩展:线程锁保护
def thread_safe(method): @wraps(method) def wrapper(self, *args, **kwargs): with self._lock: # 假设类中有 _lock 属性 return method(self, *args, **kwargs) return wrapper
七、异步函数装饰器
应用场景
- 重试机制:异步请求失败后自动重试
- 超时控制:为协程添加超时限制
- 缓存异步结果:避免重复计算
建议
- 确保
wrapper
是异步函数 - 示例扩展:异步缓存
def async_cache(func): cache = {} @wraps(func) async def wrapper(*args): if args not in cache: cache[args] = await func(*args) return cache[args] return wrapper
八、属性装饰器(@property
)
应用场景
- 数据校验:设置属性时检查合法性
- 惰性计算:延迟计算复杂属性
- 兼容性封装:将旧接口封装为属性
建议
- 避免在
@property
中执行耗时操作 - 示例扩展:惰性加载
class DataLoader: @property def data(self): if not hasattr(self, '_cached_data'): self._cached_data = self._load_data() # 耗时操作 return self._cached_data
通用建议
- 保持简单:装饰器应专注于单一功能
- 文档齐全:明确说明装饰器的作用和参数
- 性能考量:避免在装饰器中引入显著开销(如频繁 IO)
- 测试覆盖:特别注意装饰器对原函数签名的影响
通过合理选择装饰器类型,可以显著提升代码的可复用性和可维护性。
九、附录闭包(Closure)的详细解释
闭包是Python中一个非常重要的概念,特别是在装饰器的实现中起着关键作用。让我们用这个装饰器例子来彻底理解闭包:
示例代码
def repeat(num_times): # 外层函数
def decorator(func): # 中层函数
def wrapper(*args, **kwargs): # 内层函数
for _ in range(num_times): # 关键点:这里使用了num_times
result = func(*args, **kwargs)
return result
return wrapper
return decorator
一、什么是闭包?
闭包是指 一个函数能够记住并访问其词法作用域(定义时的作用域)中的变量,即使该函数在其词法作用域之外执行。
在装饰器中的体现:
- 外层变量:
num_times
(装饰器参数) - 内层函数:
wrapper
(最终被调用的函数) - 闭包关系:
wrapper
可以访问num_times
,即使repeat()
函数已经执行完毕
闭包在装饰器中的具体表现
1. 当执行 @repeat(3)
时:
decorator_func = repeat(3) # num_times=3 被保存在闭包中
- 此时
num_times=3
已经与返回的decorator
函数绑定
2. 当装饰函数时:
say_hello = decorator_func(say_hello) # 返回wrapper
- 新的闭包形成:
wrapper
可以访问:func
(原函数say_hello
)num_times=3
(来自外层)
3. 闭包验证:
print(say_hello.__closure__[0].cell_contents) # 输出:3
print(say_hello.__closure__[1].cell_contents) # 输出:<function say_hello at 0x...>
闭包的三要素
- 嵌套函数:至少两层函数嵌套(本例是三层)
- 内部函数引用外部变量:
wrapper
引用了num_times
- 外部函数返回内部函数:
repeat()
返回decorator
,decorator()
返回wrapper
为什么需要闭包?
- 保持状态:在多次调用中记住装饰器参数(
num_times=3
) - 实现封装:隐藏变量(
num_times
对调用者不可见) - 延迟绑定:参数在装饰时确定,但实际使用时才生效
闭包的内存管理
@repeat(3)
def say_hello(): pass
@repeat(5)
def say_hi(): pass
- 两个装饰器实例有独立的闭包环境:
say_hello
的闭包保存num_times=3
say_hi
的闭包保存num_times=5
闭包的实际应用场景
- 装饰器(如本例)
- 回调函数(保持上下文)
- 延迟计算
- 函数工厂(生成不同配置的函数)
与普通变量的区别
非闭包情况:
def bad_repeat(func):
num_times = 3 # 局部变量
def wrapper():
for _ in range(num_times): # 每次调用都重新读取
func()
return wrapper
问题:无法动态指定 num_times
闭包的优势:
def good_repeat(num_times): # 闭包保存这个值
def decorator(func):
def wrapper():
for _ in range(num_times): # 从闭包读取
func()
return wrapper
return decorator
闭包的底层原理
Python通过 __closure__
属性实现闭包:
# 查看闭包内容
print(say_hello.__closure__)
# 输出:(<cell at 0x...: int object at 0x...>,
# <cell at 0x...: function object at 0x...>)
每个 cell
对象保存一个被引用的外部变量。
二、 Python装饰器的完整执行过程
# 定义一个带参数的装饰器
def repeat(num_times):
# 这是一个装饰器工厂函数,它返回实际的装饰器
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
# 使用带参数的装饰器
@repeat(num_times=3)
def say_hello():
print("Hello!")
say_hello()
输出
Hello!
Hello!
Hello!
print(say_hello.__closure__[0].cell_contents) # 输出:3
print(say_hello.__closure__[1].cell_contents) # 输出:<function say_hello at 0x...>
输出
<function say_hello at 0x000001F69706E560>
3
1. 装饰阶段(Definition Time)
当Python解释器遇到 @decorator
语法时,会在函数定义时立即执行以下步骤:
① 解析装饰器语法
@repeat(num_times=3) # 解释器先执行 repeat(3),再装饰 say_hello
def say_hello(): ...
- 等价于
say_hello = repeat(3)(say_hello)
② 执行装饰器工厂
repeat(3) # 调用外层函数
- 接收参数
num_times=3
- 返回内层的
decorator
函数 - 形成闭包:
num_times=3
被保存在decorator
的环境中
③ 执行实际装饰器
decorator(say_hello) # 用返回的 decorator 处理 say_hello
- 接收原函数
say_hello
作为参数 - 定义
wrapper
函数,并通过闭包绑定:num_times=3
(来自外层)func=say_hello
(原函数)
- 返回
wrapper
函数
④ 完成函数替换
say_hello = wrapper # 原函数被替换为 wrapper
- 此时
say_hello
的__name__
变为"wrapper"
(可用@functools.wraps
修复)
2. 调用阶段(Call Time)
当调用被装饰的函数时,实际执行的是替换后的 wrapper
函数:
① 调用入口
say_hello() # 实际调用的是 wrapper()
② 执行包装逻辑
def wrapper(*args, **kwargs):
# 从闭包中读取 num_times=3
for _ in range(3):
# 通过闭包调用原函数 say_hello()
result = func(*args, **kwargs)
return result
- 从闭包获取
num_times=3
- 循环3次调用原函数
func()
(即最初的say_hello
) - 返回最后一次调用的结果
③ 执行原函数
def say_hello(): # 原始函数逻辑
print("Hello!")
- 每次循环都会执行此代码
- 若无返回值,默认返回
None
关键点总结
装饰阶段(函数定义时):
- 解析
@
语法 → 执行装饰器工厂 → 生成闭包 → 替换原函数
- 解析
调用阶段(函数调用时):
- 执行
wrapper
→ 通过闭包访问参数和原函数 → 插入额外逻辑 → 调用原函数
- 执行
闭包是核心机制:
- 同时保存了装饰器参数 (
num_times
) 和原函数 (func
) - 使得装饰器可以"记忆"配置信息
- 同时保存了装饰器参数 (
函数替换是实现基础:
- 原函数被
wrapper
替换,但wrapper
通过闭包仍能调用原函数
- 原函数被
这种设计实现了 声明式编程——只需添加 @decorator
即可修改函数行为,而无需侵入原函数代码。
@wraps
是 Python 中 functools
模块提供的一个装饰器,专门用于保留被装饰函数的原始元信息(如函数名、文档字符串、参数列表等)。它的核心作用是解决装饰器的一个常见问题:直接使用装饰器会导致原函数的元信息被替换为装饰器内部 wrapper
函数的元信息。
三、为什么需要 @wraps
?
当你不使用 @wraps
时:
def simple_decorator(func):
def wrapper(*args, **kwargs):
"""wrapper函数的docstring"""
return func(*args, **kwargs)
return wrapper
@simple_decorator
def original():
"""原始函数的docstring"""
pass
print(original.__name__) # 输出:'wrapper'(期望是 'original')
print(original.__doc__) # 输出:'wrapper函数的docstring'(期望是原始文档)
此时,原函数 original
的元信息被 wrapper
覆盖了。
使用 @wraps
的效果
from functools import wraps
def correct_decorator(func):
@wraps(func) # 关键点:将原函数的元信息复制到wrapper
def wrapper(*args, **kwargs):
"""wrapper函数的docstring"""
return func(*args, **kwargs)
return wrapper
@correct_decorator
def original():
"""原始函数的docstring"""
pass
print(original.__name__) # 输出:'original'(正确保留)
print(original.__doc__) # 输出:'原始函数的docstring'(正确保留)
@wraps
的工作原理
复制元信息:
将原函数func
的以下属性复制到wrapper
函数中:__name__
(函数名)__doc__
(文档字符串)__module__
(所属模块)__annotations__
(类型注解)__dict__
(其他自定义属性)
隐藏包装痕迹:
通过functools.WRAPPER_ASSIGNMENTS
和functools.WRAPPER_UPDATES
两个常量控制要复制的属性列表。
实际应用场景
调试和日志:
保留真实函数名便于日志追踪。@wraps(func) def wrapper(*args, **kwargs): print(f"调用函数: {func.__name__}") # 正确显示原函数名 return func(*args, **kwargs)
API 文档生成:
确保help()
或 Sphinx 生成的文档显示原函数的文档字符串。依赖函数签名的框架:
如 Flask 的路由装饰器、FastAPI 的依赖注入等需要读取函数元信息。
注意事项
必须嵌套在装饰器最内层:
@wraps(func)
应直接修饰wrapper
函数。def decorator(func): @wraps(func) # 正确位置 def wrapper(*args, **kwargs): pass return wrapper
不会自动处理参数签名:
如需完整保留参数签名(用于inspect.signature
),需额外使用functools.update_wrapper
或第三方库(如decorator
库)。
完整示例
from functools import wraps
import inspect
def debug_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"调试: 调用 {func.__name__},参数: {args}, {kwargs}")
return func(*args, **kwargs)
return wrapper
@debug_decorator
def add(a: int, b: int) -> int:
"""将两个数字相加"""
return a + b
# 测试元信息保留
print(add.__name__) # 输出: 'add'
print(add.__doc__) # 输出: '将两个数字相加'
print(inspect.signature(add)) # 输出: (a: int, b: int) -> int
输出结果:
调试: 调用 add,参数: (2, 3), {}
5
add
将两个数字相加
(a: int, b: int) -> int
总结:@wraps
是装饰器开发中的最佳实践,它能避免因装饰器引入的元信息丢失问题,确保代码的调试、文档和反射功能正常工作。