pin_drop当前位置:知识文库 ❯ 图文
Python functools.wraps详解 - 装饰器开发必备工具保留函数元信息
一、wraps 装饰器概述
functools.wraps 是 functools 模块中专门用于装饰器开发的工具函数。它的作用是将被装饰函数的元信息(如 __name__、__doc__、__module__、__annotations__ 等)复制到装饰器返回的包装函数上。
如果不使用 wraps,装饰器会掩盖原函数的身份信息,导致调试困难、文档丢失、函数内省失败等问题。wraps 是编写规范装饰器的必备工具,也是 Python 编程中的一项重要规范。
二、语法格式
代码示例
functools.wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
在大多数情况下,只需传入被装饰的函数即可,默认参数会自动处理常见的元信息复制需求。
三、参数详细说明
WRAPPER_ASSIGNMENTS 默认值为 ('__module__', '__name__', '__qualname__', '__annotations__', '__doc__')。
WRAPPER_UPDATES 默认值为 ('__dict__',)。
四、返回值说明
wraps 返回一个装饰器函数,该装饰器会将 wrapped 函数的元信息复制到被装饰的函数上。此外,包装函数的 __wrapped__ 属性会保存对原函数的引用,可以通过此属性访问原始函数。
五、代码示例详解
示例1:不使用 wraps 的问题
首先看看不使用 wraps 时会发生什么问题。下面的装饰器会丢失原函数的名称和文档字符串。
代码示例
# 不使用 wraps 的装饰器
def bad_timer(func):
def wrapper(*args, **kwargs):
"""这是wrapper的文档"""
import time
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 耗时: {end - start:.4f}秒")
return result
return wrapper
@bad_timer
def greet(name):
"""打招呼函数"""
return f"Hello, {name}!"
print(f"函数名: {greet.__name__}") # 丢失了原函数名
print(f"文档: {greet.__doc__}") # 丢失了原函数文档
# 输出:
# 函数名: wrapper
# 文档: 这是wrapper的文档
可以看到,函数名变成了 wrapper,文档也变成了包装函数的文档。这会导致调试工具和文档生成系统无法正确识别原函数。
示例2:使用 wraps 修复
现在加上 @wraps(func) 来修复这个问题。
代码示例
from functools import wraps
# 使用 wraps 的装饰器
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
import time
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} 耗时: {end - start:.4f}秒")
return result
return wrapper
@timer
def greet(name):
"""打招呼函数"""
return f"Hello, {name}!"
print(f"函数名: {greet.__name__}")
print(f"文档: {greet.__doc__}")
# 输出:
# 函数名: greet
# 文档: 打招呼函数
加上 @wraps(func) 后,函数名和文档字符串都正确保留了。
示例3:带参数的装饰器中使用 wraps
在带参数的装饰器中,wraps 同样适用。下面是一个重试装饰器的示例。
代码示例
from functools import wraps
def retry(max_attempts=3, delay=0):
"""重试装饰器:失败时自动重试"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
import time
last_error = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_error = e
print(f"第{attempt}次尝试失败: {e}")
if delay > 0 and attempt < max_attempts:
time.sleep(delay)
raise last_error
return wrapper
return decorator
@retry(max_attempts=3, delay=0.1)
def fetch_data(url):
"""从URL获取数据"""
return f"数据来自 {url}"
print(f"函数名: {fetch_data.__name__}")
print(f"文档: {fetch_data.__doc__}")
result = fetch_data("https://example.com")
print(f"结果: {result}")
# 输出:
# 函数名: fetch_data
# 文档: 从URL获取数据
# 结果: 数据来自 https://example.com
六、实际应用场景
-
装饰器开发:所有自定义装饰器都应使用
wraps,确保被装饰函数的元信息不丢失 -
调试与日志:保留原函数名和文档,使调试工具和日志系统能正确显示函数信息
-
API 文档生成:使用 Sphinx 等工具生成文档时,
wraps确保文档正确提取
七、注意事项
注意1:
wraps只能复制属性,不能复制函数签名。如果需要保留函数签名,请使用functools.wraps配合inspect.signature或第三方库wrapt。
注意2:
wraps应该应用在装饰器内部的包装函数上,而不是装饰器本身上。
注意3:
wraps会在包装函数的__wrapped__属性中保存对原函数的引用,可以通过此属性访问原始函数。
提示:养成在所有装饰器中使用
@wraps(func)的习惯,这是一个良好的编程规范。
八、与其他方法对比
在 Python 中,有多种方式可以处理装饰器的元信息保留问题。以下是不同方案的详细对比:
小贴士
functools.wraps 实际上是对 functools.update_wrapper 的装饰器封装。两者底层逻辑相同,只是用法不同。在装饰器内部使用 @wraps(func) 更加简洁优雅。如果需要手动更新属性,可以直接调用 update_wrapper(wrapper, wrapped)。
九、本章小结
-
核心作用:
wraps将被装饰函数的元信息复制到包装函数上 -
防止丢失:不使用
wraps会导致函数名、文档等信息丢失 -
编程规范:所有自定义装饰器都应使用
wraps,这是 Python 编程规范 -
签名限制:
wraps不能保留函数签名,如需完整保留可使用wrapt库
十、练习题
练习1
编写一个 @log_calls 装饰器,使用 wraps 保留原函数信息,在函数调用前后打印日志信息。
练习2
编写一个 @validate 装饰器,验证函数的第一个参数是否为正整数。使用 wraps 保留原函数元信息,并验证 __name__ 和 __doc__ 是否正确。
练习3
创建一个装饰器 @memoize,缓存函数的返回值。使用 wraps 保留原函数信息,并通过 __wrapped__ 属性访问原始函数。
常见问题
wraps 能保留函数签名吗?
不能。wraps 只能复制函数的元信息(名称、文档等),但不能复制函数签名。如果需要保留函数签名以便 IDE 能提供正确的参数提示,可以使用 Python 3.4+ 的 typing.decorator 模式或第三方库 wrapt。
__wrapped__ 属性有什么用?
__wrapped__ 属性保存了对原始函数的引用。当你需要绕过装饰器直接调用原始函数时,可以通过 func.__wrapped__() 来调用。这在测试、调试或需要访问未装饰版本函数的场景中非常有用。
为什么装饰器会导致元信息丢失?
因为装饰器本质上是返回一个新的函数对象来替代原函数。这个新函数(通常是 wrapper)有自己的 __name__、__doc__ 等属性。如果不显式复制原函数的元信息,这些属性就会是 wrapper 的默认值。
wraps 和 update_wrapper 有什么区别?
wraps 是 update_wrapper 的装饰器版本。wraps 返回一个装饰器,适合用在 @wraps(func) 的语法中;update_wrapper 是一个普通函数,需要手动调用 update_wrapper(wrapper, func)。两者底层实现相同,只是使用方式不同。
本文涉及AI创作
内容由AI创作,请仔细甄别