问题:functools.wraps是做什么的?

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

In a comment on this answer to another question, someone said that they weren’t sure what functools.wraps was doing. So, I’m asking this question so that there will be a record of it on StackOverflow for future reference: what does functools.wraps do, exactly?


回答 0

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

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将替换为function 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'

When you use a decorator, you’re replacing one function with another. In other words, if you have a decorator

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

then when you say

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

it’s exactly the same as saying

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

and your function f is replaced with the function with_logging. Unfortunately, this means that if you then say

print(f.__name__)

it will print with_logging because that’s the name of your new function. In fact, if you look at the docstring for f, it will be blank because with_logging has no docstring, and so the docstring you wrote won’t be there anymore. Also, if you look at the pydoc result for that function, it won’t be listed as taking one argument x; instead it’ll be listed as taking *args and **kwargs because that’s what with_logging takes.

If using a decorator always meant losing this information about a function, it would be a serious problem. That’s why we have functools.wraps. This takes a function used in a decorator and adds the functionality of copying over the function name, docstring, arguments list, etc. And since wraps is itself a decorator, the following code does the correct thing:

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'

回答 1

我经常将类而不是函数用于装饰器。我遇到了一些麻烦,因为对象将不具有函数期望的所有相同属性。例如,对象将没有属性__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)

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)

This class proxies all the attribute calls over to the function that is being decorated. So, you can now create a simple decorator that checks that 2 arguments are specified like so:

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

        return self.func(*args)

回答 2

从python 3.5开始:

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

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

  • 它复制__module____name____qualname____doc__,和__annotations__属性fg。此默认列表位于中WRAPPER_ASSIGNMENTS,您可以在 functools源代码中
  • 它使用中的所有元素更新__dict__of 。(看到gf.__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库相同的可靠技巧。

As of python 3.5+:

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

Is an alias for g = functools.update_wrapper(g, f). It does exactly three things:

  • it copies the __module__, __name__, __qualname__, __doc__, and __annotations__ attributes of f on g. This default list is in WRAPPER_ASSIGNMENTS, you can see it in the functools source.
  • it updates the __dict__ of g with all elements from f.__dict__. (see WRAPPER_UPDATES in the source)
  • it sets a new __wrapped__=f attribute on g

The consequence is that g appears as having the same name, docstring, module name, and signature than f. The only problem is that concerning the signature this is not actually true: it is just that inspect.signature follows wrapper chains by default. You can check it by using inspect.signature(g, follow_wrapped=False) as explained in the doc. This has annoying consequences:

  • the wrapper code will execute even when the provided arguments are invalid.
  • the wrapper code can not easily access an argument using its name, from the received *args, **kwargs. Indeed one would have to handle all cases (positional, keyword, default) and therefore to use something like Signature.bind().

Now there is a bit of confusion between functools.wraps and decorators, because a very frequent use case for developing decorators is to wrap functions. But both are completely independent concepts. If you’re interested in understanding the difference, I implemented helper libraries for both: decopatch to write decorators easily, and makefun to provide a signature-preserving replacement for @wraps. Note that makefun relies on the same proven trick than the famous decorator library.


回答 3

这是关于包装的源代码:

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)

this is the source code about wraps:

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)

回答 4

  1. 先决条件:您必须知道如何使用装饰器,尤其是包装器。该评论对其进行了解释,或者此链接也对其进行了很好的解释。

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

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

它等效于partial(update_wrapper,wraped = 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()返回简化版本,即包装函数内部的对象,而不是包装函数本身。

  1. Prerequisite: You must know how to use decorators and specially with wraps. This comment explains it a bit clear or this link also explains it pretty well.

  2. Whenever we use For eg: @wraps followed by our own wrapper function. As per the details given in this link , it says that

functools.wraps is convenience function for invoking update_wrapper() as a function decorator, when defining a wrapper function.

It is equivalent to partial(update_wrapper, wrapped=wrapped, assigned=assigned, updated=updated).

So @wraps decorator actually gives a call to functools.partial(func[,*args][, **keywords]).

The functools.partial() definition says that

The partial() is used for partial function application which “freezes” some portion of a function’s arguments and/or keywords resulting in a new object with a simplified signature. For example, partial() can be used to create a callable that behaves like the int() function where the base argument defaults to two:

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

Which brings me to the conclusion that, @wraps gives a call to partial() and it passes your wrapper function as a parameter to it. The partial() in the end returns the simplified version i.e the object of what’s inside the wrapper function and not the wrapper function itself.


回答 5

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

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

wrapper = O1 .__ call __(wrapper)

检查的执行__call__,我们看到这一步,(左侧)后包装成为对象造成的self.func(* self.args,* ARGS,** newkeywords)检查创建O1__new__,我们知道self.func是功能update_wrapper。它使用参数* args(右侧包装器)作为其第一个参数。检查update_wrapper的最后一步,可以看到返回了右侧包装器,并根据需要修改了一些属性。

In short, functools.wraps is just a regular function. Let’s consider this official example. With the help of the source code, we can see more details about the implementation and the running steps as follows:

  1. wraps(f) returns an object, say O1. It is an object of the class Partial
  2. The next step is @O1… which is the decorator notation in python. It means

wrapper=O1.__call__(wrapper)

Checking the implementation of __call__, we see that after this step, (the left hand side )wrapper becomes the object resulted by self.func(*self.args, *args, **newkeywords) Checking the creation of O1 in __new__, we know self.func is the function update_wrapper. It uses the parameter *args, the right hand side wrapper, as its 1st parameter. Checking the last step of update_wrapper, one can see the right hand side wrapper is returned, with some of attributes modified as needed.


声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。