协慌网

登录 贡献 社区

functools.wraps 是做什么的?

在对另一个问题的答案发表评论时,有人说他们不确定functools.wraps在做什么。所以,我问这个问题,以便在 StackOverflow 上有它的记录,以备将来参考: functools.wraps到底是做什么的?

答案

使用装饰器时,您将一个功能替换为另一个功能。换句话说,如果您有一个装饰器

def logged(func):
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

然后当你说

@logged
def f(x):
   """does some math"""
   return x + x * x

这和说的完全一样

def f(x):
    """does some math"""
    return x + x * x
f = logged(f)

并且将函数f替换为with_logging函数。不幸的是,这意味着如果您然后说

print(f.__name__)

它将显示with_logging因为这是新函数的名称。实际上,如果您查看f的文档字符串,它将为空,因为with_logging没有文档字符串,因此您编写的文档字符串将不再存在。另外,如果您查看该函数的 pydoc 结果,它将不会被列为带有一个参数x ;相反,它将被列为*args**kwargs因为 with_logging 需要这样做。

如果使用装饰器总是意味着丢失有关功能的信息,那将是一个严重的问题。这就是为什么我们有functools.wraps的原因。它采用了装饰器中使用的功能,并添加了复制功能名称,文档字符串,参数列表等的功能。由于wraps本身就是装饰器,因此以下代码可以正确执行操作:

from functools import wraps
def logged(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print(func.__name__ + " was called")
        return func(*args, **kwargs)
    return with_logging

@logged
def f(x):
   """does some math"""
   return x + x * x

print(f.__name__)  # prints 'f'
print(f.__doc__)   # prints 'does some math'

我经常将类而不是函数用于我的装饰器。我遇到了一些麻烦,因为对象将不具有函数期望的所有相同属性。例如,一个对象将没有属性__name__ 。我有一个特定的问题,很难跟踪 Django 报告错误 “对象没有属性' __name__ '” 的位置。不幸的是,对于类风格的装饰器,我认为 @wrap 不会胜任。相反,我创建了一个基本的装饰器类,如下所示:

class DecBase(object):
    func = None

    def __init__(self, func):
        self.__func = func

    def __getattribute__(self, name):
        if name == "func":
            return super(DecBase, self).__getattribute__(name)

        return self.func.__getattribute__(name)

    def __setattr__(self, name, value):
        if name == "func":
            return super(DecBase, self).__setattr__(name, value)

        return self.func.__setattr__(name, value)

此类将所有属性调用代理到要修饰的函数。因此,您现在可以创建一个简单的装饰器来检查是否指定了 2 个参数,如下所示:

class process_login(DecBase):
    def __call__(self, *args):
        if len(args) != 2:
            raise Exception("You can only specify two arguments")

        return self.func(*args)

从 python 3.5 开始:

@functools.wraps(f)
def g():
    pass

g = functools.update_wrapper(g, f)的别名。它确实完成了三件事:

  • 它复制__module____name____qualname____doc__ ,和__annotations__的属性fg 。该默认列表位于WRAPPER_ASSIGNMENTS ,您可以在functools 源代码中看到它。
  • f.__dict__所有元素g __dict__ 。 (请参阅源代码中的WRAPPER_UPDATES
  • 它在g上设置了新的__wrapped__=f

结果是g f具有相同的名称,文档字符串,模块名称和签名。唯一的问题是,关于签名,这实际上并不正确:只是inspect.signature遵循包装器链。您可以使用doc中所述的inspect.signature(g, follow_wrapped=False)进行检查。这会产生令人讨厌的后果:

  • 即使提供的参数无效,包装器代码也将执行。
  • 包装器代码无法轻松地从接收到的 * args,** kwargs 中使用其名称访问参数。实际上,必须处理所有情况(位置,关键字,默认值),并因此使用诸如Signature.bind()类的东西。

functools.wraps和装饰器之间有些混淆,因为开发装饰器的一个非常常见的用例是包装函数。但是两者都是完全独立的概念。如果您有兴趣了解它们之间的区别,则可以为这两种方法都实现帮助程序库:decopatch 可以轻松编写装饰器, makemake可以为 @wraps 提供保留签名的替代@wraps 。请注意, makefun依赖于与著名的decorator库相同的可靠技巧。