What does functools.wraps do?

前端 未结 6 784
北海茫月
北海茫月 2020-11-22 06:23

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 ther

相关标签:
6条回答
  • 2020-11-22 06:47
    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.

    0 讨论(0)
  • 2020-11-22 06:50

    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)
    
    0 讨论(0)
  • 2020-11-22 06:52

    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'
    
    0 讨论(0)
  • 2020-11-22 06:56

    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)
    
    0 讨论(0)
  • 2020-11-22 06:59

    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.

    0 讨论(0)
  • 2020-11-22 07:10

    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.

    0 讨论(0)
提交回复
热议问题