Python @functools.wraps保留被装饰函数元信息
编写 Python 装饰器时,如果不做特殊处理,被装饰函数的元信息(如函数名、文档字符串、参数注解等)会被替换为装饰器内部包装函数的信息。这会导致调试困难、文档生成错误以及基于函数签名的操作失效。functools.wraps 正是用于解决这一问题的标准工具。
1. 创建基础测试环境
打开 你的代码编辑器或 IDE,新建 一个名为 test_wraps.py 的文件。后续的操作将在此文件中进行。
2. 编写一个无装饰器的原始函数
为了对比效果,首先定义一个标准的函数,包含完整的文档字符串和类型注解。
输入 以下代码:
def add(a: int, b: int) -> int:
"""
计算两个整数的和。
Args:
a: 第一个整数
b: 第二个整数
Returns:
两个整数的和
"""
return a + b
# 测试原始函数的元信息
if __name__ == "__main__":
print(f"函数名: {add.__name__}")
print(f"文档字符串: {add.__doc__}")
print(f"参数注解: {add.__annotations__}")
运行 脚本,观察控制台输出。此时 __name__ 是 add,__doc__ 是定义的说明文档,一切正常。
3. 编写未使用 @wraps 的装饰器
现在编写一个简单的日志装饰器,它不使用 functools.wraps。
添加 以下代码到 test_wraps.py:
import time
# 一个不使用 @wraps 的装饰器
def simple_timer(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
# 应用装饰器
@simple_timer
def subtract(a: int, b: int) -> int:
"""
计算两个整数的差。
Args:
a: 被减数
b: 减数
Returns:
两个整数的差
"""
return a - b
# 测试被装饰后的函数元信息
print("\n--- 未使用 @wraps 的结果 ---")
print(f"函数名: {subtract.__name__}")
print(f"文档字符串: {subtract.__doc__}")
print(f"参数注解: {subtract.__annotations__}")
保存 并 运行 脚本。
注意 输出结果中的异常:
函数名变成了wrapper。文档字符串变成了wrapper的代码(通常是None)。参数注解变成了空字典{}。
这说明 subtract 函数的“身份”已经被 wrapper 函数完全覆盖,这对于依赖元信息的工具(如 Sphinx 文档生成、调试器)是灾难性的。
4. 引入 @functools.wraps 修复问题
修改 上一步的装饰器代码,引入 functools 模块,并在 wrapper 函数定义上方 添加 @wraps(func) 装饰器。
替换 simple_timer 相关代码如下:
import functools
import time
# 使用 @wraps 修复后的装饰器
def smart_timer(func):
@functools.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
# 应用修复后的装饰器
@smart_timer
def multiply(a: int, b: int) -> int:
"""
计算两个整数的积。
Args:
a: 乘数
b: 乘数
Returns:
两个整数的积
"""
return a * b
# 测试修复后的函数元信息
print("\n--- 使用 @wraps 修复后的结果 ---")
print(f"函数名: {multiply.__name__}")
print(f"文档字符串: {multiply.__doc__}")
print(f"参数注解: {multiply.__annotations__}")
保存 并 运行 脚本。
确认 输出结果:
函数名恢复为multiply。文档字符串恢复为原本的中文说明。参数注解恢复为{'a': <class 'int'>, 'b': <class 'int'>, 'return': <class 'int'>}。
5. 理解 @wraps 的工作原理
@functools.wraps 实际上是一个偏函数应用,它底层调用的是 functools.update_wrapper 函数。
当我们 调用 @functools.wraps(func) 时,它主要执行了以下操作:
- 复制 模块属性:将
func的__module__、__name__、__qualname__、__annotations__和__doc__属性复制给wrapper。 - 更新
__dict__:将func的__dict__属性更新到wrapper中。 - 隐藏 原始函数:在
wrapper上设置__wrapped__属性,指向原始的func对象。
这一过程可以用以下流程图表示:
6. 利用 __wrapped__ 属性进行逆向查找
使用 @wraps 后,Python 会在包装函数上添加一个 __wrapped__ 属性。这对于调试或需要绕过装饰器直接访问原始函数的场景非常有用。
添加 以下测试代码:
print("\n--- 测试 __wrapped__ 属性 ---")
print(f"multiply 是 multiply.__wrapped__ 吗? {multiply is multiply.__wrapped__}")
print(f"原始函数名: {multiply.__wrapped__.__name__}")
运行 代码,可以看到虽然外部调用的是 multiply(即包装后的 wrapper),但通过 .__wrapped__ 我们依然可以触及原始的函数对象。
7. 处理带参数的装饰器
在实际开发中,装饰器往往需要接收参数(例如 @retry(times=3))。@wraps 同样适用于这种嵌套结构。
编写 一个带参数的装饰器示例:
def repeat(num_times):
def decorator_repeat(func):
@functools.wraps(func)
def wrapper_repeat(*args, **kwargs):
# 这里的 __name__ 会正确显示为 func 的名字,而不是 wrapper_repeat
print(f"准备执行 {func.__name__} {num_times} 次")
result = None
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper_repeat
return decorator_repeat
@repeat(num_times=3)
def greet(name):
"""向某人打招呼"""
print(f"Hello, {name}!")
print("\n--- 带参数装饰器的元信息 ---")
print(f"函数名: {greet.__name__}")
print(f"文档字符串: {greet.__doc__}")
greet("Alice")
观察 输出,即使装饰器嵌套了三层(repeat -> decorator_repeat -> wrapper_repeat),最内层的 @functools.wraps(func) 依然成功将 greet 的元信息传递到了最外层。
8. 最佳实践检查清单
在编写生产环境代码时,遵循 以下清单以确保元信息处理的正确性:
- 始终导入
import functools。 - 始终在 内部
wrapper函数上 使用@functools.wraps(func)。 - 检查
__wrapped__属性是否被正确设置,以便于调试。 - 避免 在
wrapper函数中手动覆盖__name__或__doc__,应完全交给wraps处理。
9. 常见错误排查
如果在使用 @wraps 后发现元信息依然丢失,排查 以下两点:
- 位置错误:确认
@functools.wraps(func)是紧贴在内部def wrapper(...):上方的,而不是在外层装饰器函数上。 - 函数对象问题:确认被装饰的对象确实是函数,如果装饰类方法,通常
wraps依然有效,但需确保self参数处理正确。
# 错误示例:位置放错了
def my_decorator(func):
@functools.wraps(func) # 错!这里装饰的是 my_decorator 本身
def inner():
return func()
return inner
# 正确示例
def my_decorator(func):
@functools.wraps(func) # 对!这里装饰的是 wrapper
def wrapper():
return func()
return wrapper
10. 完整代码参考
以下是本文涉及的完整可运行代码:
import functools
import time
# 1. 原始函数
def add(a: int, b: int) -> int:
"""
计算两个整数的和。
"""
return a + b
# 2. 未使用 @wraps 的装饰器
def simple_timer(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
@simple_timer
def subtract(a: int, b: int) -> int:
"""计算两个整数的差。"""
return a - b
# 3. 使用 @wraps 的装饰器
def smart_timer(func):
@functools.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
@smart_timer
def multiply(a: int, b: int) -> int:
"""计算两个整数的积。"""
return a * b
# 4. 带参数的装饰器
def repeat(num_times):
def decorator_repeat(func):
@functools.wraps(func)
def wrapper_repeat(*args, **kwargs):
for _ in range(num_times):
result = func(*args, **kwargs)
return result
return wrapper_repeat
return decorator_repeat
@repeat(num_times=2)
def greet(name):
"""向某人打招呼"""
print(f"Hello, {name}!")
if __name__ == "__main__":
print("--- 原始函数 ---")
print(f"Name: {add.__name__}, Doc: {add.__doc__}")
print("\n--- 未使用 @wraps ---")
# 这里的 subtract 实际上指向 wrapper
print(f"Name: {subtract.__name__}, Doc: {subtract.__doc__}")
print("\n--- 使用 @wraps ---")
print(f"Name: {multiply.__name__}, Doc: {multiply.__doc__}")
print(f"Original via __wrapped__: {multiply.__wrapped__.__name__}")
print("\n--- 带参数装饰器 ---")
print(f"Name: {greet.__name__}, Doc: {greet.__doc__}")
greet("Bob")
暂无评论,快来抢沙发吧!