838

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

4

7 回答 7

1340

当您使用装饰器时,您正在用另一个功能替换一个功能。换句话说,如果你有一个装饰器

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'
于 2008-11-21T14:53:47.410 回答
43

从 python 3.5+ 开始:

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

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

  • 它复制on的__module__, __name__, __qualname__,__doc____annotations__属性。这个默认列表在里面,你可以在functools 源码中看到。fgWRAPPER_ASSIGNMENTS
  • 它使用来自__dict__g所有元素更新f.__dict__。(见WRAPPER_UPDATES源代码)
  • 它设置了一个新__wrapped__=f属性g

结果是它g显示为具有相同的名称、文档字符串、模块名称和签名f。唯一的问题是关于签名这实际上不是真的:它只是inspect.signature默认遵循包装链。您可以按照文档inspect.signature(g, follow_wrapped=False)中的说明使用来检查它。这会带来烦人的后果:

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

现在和装饰器之间有点混淆functools.wraps,因为开发装饰器的一个非常常见的用例是包装函数。但两者都是完全独立的概念。如果您有兴趣了解其中的区别,我为两者实现了帮助程序库:decopatch可以轻松编写装饰器,makefun可以为@wraps. 请注意,它makefun依赖于与著名decorator库相同的经过验证的技巧。

于 2019-03-11T13:16:09.837 回答
28

我经常为我的装饰器使用类,而不是函数。我遇到了一些麻烦,因为对象不会具有函数所期望的所有相同属性。例如,一个对象将没有属性__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)
于 2009-12-03T23:46:43.537 回答
7
  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
  1. 运行 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
  1. 现在我们再次运行第 5 步(第二次):
>>> 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 (decotator 工具)
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

参考

于 2021-11-12T21:58:36.433 回答
4
  1. 先决条件:您必须知道如何使用装饰器,尤其是包装。这个评论解释得有点清楚,或者这个链接也很好地解释了它。

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

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

它相当于部分(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() 返回简化版本,即包装函数内部的对象,而不是包装函数本身。

于 2018-03-10T03:14:01.283 回答
4

这是关于包装的源代码:

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)
于 2018-12-14T06:01:24.013 回答
-4

简而言之,functools.wraps只是一个常规函数。让我们考虑这个官方示例。借助源代码,我们可以看到更多的实现细节和运行步骤,如下所示:

  1. wraps(f)返回一个对象,比如O1。它是Partial 类的对象
  2. 下一步是@O1...,它是 python 中的装饰符号。它的意思是

包装器=O1.__call__(包装器)

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

于 2018-03-30T04:27:08.427 回答