functools.wraps是什么?

973

在另一个问题的答案评论中,有人说他们不确定functools.wraps在做什么。所以,我提出这个问题,以便在 StackOverflow 上留下记录供将来参考: functools.wraps具体是做什么?

7个回答

1553

使用装饰器时,您正在用另一个函数替换一个函数。换句话说,如果您有一个装饰器

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的docstring,它将为空,因为with_logging没有docstring,因此您编写的docstring将不再存在。此外,如果您查看该函数的pydoc结果,则不会将其列为接受一个参数x的函数;相反,它将被列为接受*args**kwargs,因为这就是with_logging需要的。

如果使用装饰器总是意味着失去关于函数的这些信息,那么这将是一个严重的问题。这就是为什么我们有functools.wraps的原因。它接受在装饰器中使用的函数,并添加复制函数名称、docstring、参数列表等功能。而且由于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'

11
是的,我更倾向于避免使用装饰器模块,因为functools.wraps已经包含在标准库中,不会引入其他依赖。但是装饰器模块确实可以解决帮助文档的问题,希望functools.wraps也能够在未来解决这个问题。 - Eli Courtwright
9
如果不使用wraps,以下是可能发生的情况:doctools测试可能会突然消失。这是因为装饰函数中的测试,除非类似wraps()的东西已将它们复制过来,否则doctools无法找到它们。 - andrew cooke
151
为什么我们需要使用 functools.wraps 进行这项工作,难道它不应该一开始就作为装饰器模式的一部分吗?何时您不想使用 @wraps? - wim
92
@wim: 我已经编写了一些装饰器,它们在自己的版本中执行@wraps,以执行各种类型的修改或注释。基本上,这是Python哲学的扩展,即显式优于隐式,并且特例不足以打破规则。(如果必须手动提供@wraps而不是使用某种特殊的退出机制,则代码要简单得多,语言更容易理解。) - ssokolow
64
并不是所有的装饰器都会包装它们所修饰的函数。有些会应用一些副作用,比如在某种查找系统中注册它们。 - ssokolow
显示剩余7条评论

64

从 Python 3.5+ 开始:

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

g = functools.update_wrapper(g, f)的别名。它做了三件事:

  • 它将 f__module__, __name__, __qualname__, __doc__, 和 __annotations__ 属性复制到 g。这个默认列表在WRAPPER_ASSIGNMENTS中,你可以在functools源代码中看到。
  • 它更新 g__dict__ 来自于 f.__dict__ 中的所有元素。(请看源代码中的 WRAPPER_UPDATES)
  • 它在g上设置一个新的__wrapped__=f属性

其结果是g看起来具有与f相同的名称、文档字符串、模块名称和签名。唯一的问题是关于签名,实际上这不是真的:只是因为inspect.signature默认跟随包装器链。您可以通过使用inspect.signature(g, follow_wrapped=False)来验证它,如文档中所述。这带来了烦人的后果:

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

现在functools.wraps和装饰器之间有些混淆,因为开发装饰器的一个非常常见的用例是包装函数。但两者完全独立的概念。如果您想了解差异,我为两者都实现了辅助库: decopatch 用于轻松编写装饰器,makefun 则提供了保留签名的替代@wraps。请注意,makefun依赖于与著名的decorator库相同的成熟技巧。


43
  1. 假设我们有一个简单的装饰器,它将函数的输出放入一个字符串中,并在后面加上三个感叹号。
def mydeco(func):
    def wrapper(*args, **kwargs):
        return f'{func(*args, **kwargs)}!!!'
    return wrapper
  1. 现在让我们用“mydeco”来装饰两个不同的函数:
@mydeco
def add(a, b):
    '''Add two objects together, the long way'''
    return a + b

@mydeco
def mysum(*args):
    '''Sum any numbers together, the long way'''
    total = 0
    for one_item in args:
        total += one_item
    return total

当运行add(10,20),mysum(1,2,3,4)时,它们都成功了!
>>> add(10,20)
'30!!!'

>>> mysum(1,2,3,4)
'10!!!!'
  1. 然而,name属性在我们定义函数时为函数指定名称,
>>>add.__name__
'wrapper`

>>>mysum.__name__
'wrapper'
  1. 更糟糕
>>> help(add)
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)

>>> help(mysum)
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)
  1. 我们可以通过以下方式部分解决:
def mydeco(func):
    def wrapper(*args, **kwargs):
        return f'{func(*args, **kwargs)}!!!'
    wrapper.__name__ = func.__name__
    wrapper.__doc__ = func.__doc__
    return wrapper

现在我们再次运行第5步(第2次):
>>> help(add)
Help on function add in module __main__:

add(*args, **kwargs)
     Add two objects together, the long way

>>> help(mysum)
Help on function mysum in module __main__:

mysum(*args, **kwargs)
    Sum any numbers together, the long way

  1. 但我们可以使用functools.wraps(装饰器工具)

from functools import wraps

def mydeco(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return f'{func(*args, **kwargs)}!!!'
    return wrapper
  1. 现在再次运行第5步(第3次)
>>> help(add)
Help on function add in module main:
add(a, b)
     Add two objects together, the long way

>>> help(mysum)
Help on function mysum in module main:
mysum(*args)
     Sum any numbers together, the long way

参考链接


30

我经常使用类来作为装饰器,而不是函数。但由于对象没有函数所期望的所有属性,这给我带来了一些困扰。例如,对象不会有属性__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)

12
根据@wraps文档的说明,@wraps只是一个使用方便的函数,它调用了functools.update_wrapper()。在类装饰器的情况下,您可以直接从__init__()方法中调用update_wrapper()。因此,您根本不需要创建DecBase类,只需在process_login__init__()方法中添加以下代码行:update_wrapper(self, func)。就这样。 - Fabiano
只是让其他人也能找到这个答案:Flask中使用add_url_route时,需要(在某些情况下?)提供的view_func函数具有__name__属性。如果提供的函数实际上是一个装饰方法,即使使用了 functools.wraps 装饰器,该属性也不再存在。 - Joël
因此,对于@Fabiano来说加1:使用update_wrapper而不是@wraps就可以完成工作 :) - Joël

6
  1. 先决条件:您必须知道如何使用装饰器,特别是使用wraps。这个评论解释得很清楚,或者这个链接也解释得非常好。

  2. 每当我们使用例如:@wraps 紧接着我们自己的包装器函数时。根据此链接中给出的详细信息,它说

functools.wraps 是一个方便的函数,用于在定义包装器函数时作为函数装饰器调用update_wrapper()。

它等同于partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)。

因此,@wraps 装饰器实际上会调用 functools.partial(func[,*args][, **keywords])。

functools.partial() 的定义是

partial() 用于部分函数应用程序,其中“冻结”了函数的某些参数和/或关键字,从而产生具有简化签名的新对象。例如,partial() 可用于创建一个可调用对象,其行为类似于 int() 函数,其中基础参数默认为二:

>>> from functools import partial
>>> basetwo = partial(int, base=2)
>>> basetwo.__doc__ = 'Convert base 2 string to an int.'
>>> basetwo('10010')
18

这让我得出结论:@wraps会调用partial()并将您的包装函数作为参数传递给它。最终,partial()返回简化版本即包装函数内部的对象而不是包装函数本身。


6

这是有关包装的源代码:

WRAPPER_ASSIGNMENTS = ('__module__', '__name__', '__doc__')

WRAPPER_UPDATES = ('__dict__',)

def update_wrapper(wrapper,
                   wrapped,
                   assigned = WRAPPER_ASSIGNMENTS,
                   updated = WRAPPER_UPDATES):

    """Update a wrapper function to look like the wrapped function

       wrapper is the function to be updated
       wrapped is the original function
       assigned is a tuple naming the attributes assigned directly
       from the wrapped function to the wrapper function (defaults to
       functools.WRAPPER_ASSIGNMENTS)
       updated is a tuple naming the attributes of the wrapper that
       are updated with the corresponding attribute from the wrapped
       function (defaults to functools.WRAPPER_UPDATES)
    """
    for attr in assigned:
        setattr(wrapper, attr, getattr(wrapped, attr))
    for attr in updated:
        getattr(wrapper, attr).update(getattr(wrapped, attr, {}))
    # Return the wrapper so this can be used as a decorator via partial()
    return wrapper

def wraps(wrapped,
          assigned = WRAPPER_ASSIGNMENTS,
          updated = WRAPPER_UPDATES):
    """Decorator factory to apply update_wrapper() to a wrapper function

   Returns a decorator that invokes update_wrapper() with the decorated
   function as the wrapper argument and the arguments to wraps() as the
   remaining arguments. Default arguments are as for update_wrapper().
   This is a convenience function to simplify applying partial() to
   update_wrapper().
    """
    return partial(update_wrapper, wrapped=wrapped,
                   assigned=assigned, updated=updated)

-6

简而言之,functools.wraps 只是一个普通函数。让我们看看官方示例。通过源代码的帮助,我们可以了解到实现和运行步骤的更多细节如下:

  1. wraps(f) 返回一个对象,比如说O1。它是Partial类的一个对象。
  2. 接下来是@O1...,这是Python中的装饰器符号。它表示

wrapper=O1.__call__(wrapper)

检查__call__的实现,我们可以看到在这一步之后,(左侧)wrapper变成了由self.func(*self.args, *args, **newkeywords)产生的对象。检查__new__O1的创建,我们知道self.func是函数update_wrapper。它使用参数*args,右侧的wrapper作为其第一个参数。检查update_wrapper的最后一步,可以看到右侧的wrapper被返回,并根据需要修改了一些属性。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接