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)

在大多数情况下,只需传入被装饰的函数即可,默认参数会自动处理常见的元信息复制需求。

三、参数详细说明

参数 类型 必填 默认值 说明
wrapped 可调用对象 被装饰的原始函数
assigned tuple WRAPPER_ASSIGNMENTS 需要从原函数复制的属性名元组
updated tuple 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 确保文档正确提取

七、注意事项

注意1wraps 只能复制属性,不能复制函数签名。如果需要保留函数签名,请使用 functools.wraps 配合 inspect.signature 或第三方库 wrapt

注意2wraps 应该应用在装饰器内部的包装函数上,而不是装饰器本身上。

注意3wraps 会在包装函数的 __wrapped__ 属性中保存对原函数的引用,可以通过此属性访问原始函数。

提示:养成在所有装饰器中使用 @wraps(func) 的习惯,这是一个良好的编程规范。

八、与其他方法对比

在 Python 中,有多种方式可以处理装饰器的元信息保留问题。以下是不同方案的详细对比:

特性 functools.wraps functools.update_wrapper 手动复制属性 第三方 wrapt
使用方式 装饰器 函数调用 手动赋值 装饰器
保留函数名 需手动
保留文档 需手动
保留函数签名 需手动
易用性 极高
典型用途 装饰器内使用 手动更新属性 特殊需求 完整装饰器方案

小贴士

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)。两者底层实现相同,只是使用方式不同。

标签: wraps 装饰器 functools 元信息 Python编程规范

本文涉及AI创作

内容由AI创作,请仔细甄别

list快速访问

上一篇: Python functools.partial偏函数详解 - 固定参数创建可调用对象 下一篇: Python functools.lru_cache详解 - LRU缓存优化函数性能

poll相关推荐