在对另一个问题的回答的评论中,有人说他们不确定functools是什么。Wraps在做什么。所以,我问这个问题是为了在StackOverflow上有一个记录,以供将来参考:functools。包裹真的有用吗?


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

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'

I very often use classes, rather than functions, for my decorators. I was having some trouble with this because an object won't have all the same attributes that are expected of a function. For example, an object won't have the attribute __name__. I had a specific issue with this that was pretty hard to trace where Django was reporting the error "object has no attribute '__name__'". Unfortunately, for class-style decorators, I don't believe that @wrap will do the job. I have instead created a base decorator class like so:

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)

前提条件:你必须知道如何使用装饰,特别是包装。这个评论解释得很清楚,或者这个链接也解释得很好。 当我们使用For eg: @ wrapper函数时。根据这个链接中给出的细节,它说

functools。Wraps是一个方便的函数,用于在定义包装器函数时调用update_wrapper()作为函数装饰器。 它等价于partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated)。

@wraps decorator实际上给了functools一个调用。部分(func [* args][, * *关键字])。

functools.partial()定义是这样说的

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

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

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


简而言之,就是functools。Wraps只是一个普通的函数。让我们考虑这个官方的例子。在源代码的帮助下,我们可以看到关于实现和运行步骤的更多细节,如下所示:

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

包装器= O1.__call__(包装)

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


这是关于包装的源代码:

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)

从python 3.5+开始:

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

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

它复制了f on g的__module__, __name__, __qualname__, __doc__和__annotations__属性。这个默认列表在WRAPPER_ASSIGNMENTS中,你可以在functools源代码中看到它。 它用f.__dict__中的所有元素更新g的__dict__。(请参阅源代码中的WRAPPER_UPDATES) 它在g上设置了一个新的__wrapped__=f属性

结果是g看起来与f具有相同的名称、文档字符串、模块名称和签名。唯一的问题是,关于签名,这实际上不是真的:它只是inspect。默认情况下签名遵循包装器链。你可以使用inspect来检查它。signature(g, follow_wrapped=False),如文档中解释的那样。这有令人讨厌的后果:

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

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


假设我们有这个:Simple Decorator,它接受一个函数的输出并将其放入一个字符串中,后面跟着三个!!!!。

def mydeco(func):
    def wrapper(*args, **kwargs):
        return f'{func(*args, **kwargs)}!!!'
    return wrapper

现在让我们用“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!!!!'

然而,name属性在定义函数时给出了它的名称,

>>>add.__name__
'wrapper`

>>>mysum.__name__
'wrapper'

更糟糕的是

>>> help(add)
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)

>>> help(mysum)
Help on function wrapper in module __main__:
wrapper(*args, **kwargs)

我们可以通过以下方法进行部分修复:

def mydeco(func):
    def wrapper(*args, **kwargs):
        return f'{func(*args, **kwargs)}!!!'
    wrapper.__name__ = func.__name__
    wrapper.__doc__ = func.__doc__
    return wrapper

现在我们再次运行第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

但是我们可以使用functools。包装(蒸煮工具)

from functools import wraps

def mydeco(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return f'{func(*args, **kwargs)}!!!'
    return wrapper

现在再次运行第5步(第三次)

>>> 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

参考