functools.wraps 有什么作用?

发布于 2024-07-08 14:06:03 字数 274 浏览 10 评论 0 原文

在对此另一个答案的评论中问题,有人说他们不确定 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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

情未る 2024-07-15 14:06:03

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

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'

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'
感性不性感 2024-07-15 14:06:03

从 python 3.5+ 开始:

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

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

  • 复制 __module____name____qualname____doc____annotations__ gf 属性。 这个默认列表位于 WRAPPER_ASSIGNMENTS 中,您可以在 functools 源
  • 它使用 f.__dict__ 中的所有元素更新 g__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 库相同的经过验证的技巧。

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.

遗忘曾经 2024-07-15 14:06:03
  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。包裹(decotator工具)
from functools import wraps

def mydeco(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return f'{func(*args, **kwargs)}!!!'
    return wrapper
  1. 现在再次运行步骤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

参考

  1. Assume we have this: Simple Decorator which takes a function’s output and puts it into a string, followed by three !!!!.
def mydeco(func):
    def wrapper(*args, **kwargs):
        return f'{func(*args, **kwargs)}!!!'
    return wrapper
  1. Let’s now decorate two different functions with “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. when run add(10,20), mysum(1,2,3,4), it worked!
>>> add(10,20)
'30!!!'

>>> mysum(1,2,3,4)
'10!!!!'
  1. However, the name attribute, which gives us the name of a function when we define it,
>>>add.__name__
'wrapper`

>>>mysum.__name__
'wrapper'
  1. Worse
>>> 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. we can fix partially by:
def mydeco(func):
    def wrapper(*args, **kwargs):
        return f'{func(*args, **kwargs)}!!!'
    wrapper.__name__ = func.__name__
    wrapper.__doc__ = func.__doc__
    return wrapper
  1. now we run step 5 (2nd time) again:
>>> 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. but we can use functools.wraps (decotator tool)
from functools import wraps

def mydeco(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return f'{func(*args, **kwargs)}!!!'
    return wrapper
  1. now run step 5 (3rd time) again
>>> 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

Reference

巨坚强 2024-07-15 14:06:03

我经常为我的装饰器使用类,而不是函数。 我在这方面遇到了一些麻烦,因为对象不会具有函数所期望的所有相同属性。 例如,对象不会具有属性 __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)
非要怀念 2024-07-15 14:06:03
  1. 先决条件:您必须知道如何使用装饰器,特别是包装器。 这个评论解释得有点清楚或者这个链接也很好地解释了它。

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

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

它相当于partial(update_wrapper,wrapped=wrapped,signed=assigned,updated=updated)。

所以@wraps装饰器实际上调用了functools.partial(func[,*args][,**keywords])。

functools.partial() 定义表示

partial() 用于部分函数应用程序,它“冻结”函数参数和/或关键字的某些部分,从而产生具有简化签名的新对象。 例如,partial() 可用于创建一个可调用函数,其行为类似于 int() 函数,其中基本参数默认为 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() 最后返回简化版本,即包装函数内部的对象,而不是包装函数本身。

  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.

丶视觉 2024-07-15 14:06:03

这是关于包装的源代码:

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)
夏九 2024-07-15 14:06:03

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

  1. wraps(f) 返回一个对象,例如O1。 它是 类 Partial
  2. 下一步是@O1...,这是Python 中的装饰器表示法。 它的意思是

wrapper=O1.__call__(包装器)

检查 __call__,我们看到在这一步之后,(左侧)wrapper 变成了 self.func 结果的对象(*self.args, *args, **newkeywords) 检查__new__O1的创建,我们知道self.func > 是函数update_wrapper。 它使用参数*args(右侧包装器)作为其第一个参数。 检查update_wrapper的最后一步,可以看到返回了右侧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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文