What does functools.wraps do?

194,282

Solution 1

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'

Solution 2

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.

Solution 3

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)

Solution 4

  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

Solution 5

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)
Share:
194,282
Eli Courtwright
Author by

Eli Courtwright

I'm a programmer, roleplayer, Sunday school teacher, and MAGFest organizer, among other things. I'm also the author of some open source Python modules: protlib: for implementing binary network protocols. collectd: for sending statistics to collectd servers over UDP

Updated on July 27, 2022

Comments

  • Eli Courtwright
    Eli Courtwright almost 2 years

    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?

  • Scott Griffiths
    Scott Griffiths over 14 years
    There's a solution to the help problem which uses the decorator module, and is mentioned in the comments on this answer: stackoverflow.com/questions/1782843/…
  • Eli Courtwright
    Eli Courtwright over 14 years
    Yep, I prefer to avoid the decorator module since functools.wraps is part of the standard library and thus doesn't introduce another external dependency. But the decorator module does indeed solve the help problem, which hopefully functools.wraps someday will as well.
  • andrew cooke
    andrew cooke about 13 years
    here's an example of what can happen if you don't use wraps: doctools tests can suddenly disappear. that's because doctools cannot find the tests in decorated functions unless something like wraps() has copied them across.
  • wim
    wim over 10 years
    why do we need functools.wraps for this job, shouldn't it just be part of the decorator pattern in the first place? when would you not want to use @wraps ?
  • ssokolow
    ssokolow about 10 years
    @wim: I've written some decorators which do their own version of @wraps in order to perform various types of modification or annotation on the values copied over. Fundamentally, it's an extension of the Python philosophy that explicit is better than implicit and special cases aren't special enough to break the rules. (The code is much simpler and the language easier to understand if @wraps must be provided manually, rather than using some kind of special opt-out mechanism.)
  • Marco Sulla
    Marco Sulla almost 9 years
    @ssokolow: special cases aren't special enough to break the rules: if the wraps was the default behaviour, that will be the "rule". wraps sounds to me more like a patch that a more explicit way to do things. That said, it's better to add a patch instead of changing the existing rules and create Python 4 with a parterre of 5 users.
  • ssokolow
    ssokolow almost 9 years
    @LucasMalor Not all decorators wrap the functions they decorate. Some apply side-effects, such as registering them in some kind of lookup system.
  • Rajendra Uppal
    Rajendra Uppal almost 7 years
    but why do we need to restore function name, docstring, etc. and looks like @wraps is then a patch to the decorator pattern. what is the danger, what do we loose? what functionality cannot be given if correct func name, docstring etc. is not there? couldn't it be implemented built-in even later when this problem was detected? what backward compatibility issues prevented from implementing within the lang itself?
  • McKay
    McKay over 6 years
    How would the compiler even know that it is a decorator to know when to apply @wraps automatically?
  • bbarker
    bbarker about 6 years
    Seems like a decorator is potentially a common way to implement the function-map part of a Functor ( en.wikipedia.org/wiki/Functor ) in Python, though I guess that idea by itself isn't very useful from a categorical perspective
  • Fabiano
    Fabiano over 5 years
    As the docs from @wraps says, @wraps is just a convenience function to functools.update_wrapper(). In case of class decorator, you can call update_wrapper() directly from your __init__() method. So, you don't need to create DecBase at all, you can just include on __init__() of process_login the line: update_wrapper(self, func). That's all.
  • Joël
    Joël over 3 years
    Just so that others find this answer as well: Flask, with its add_url_route, requires (in some cases?) that the provided view_func function has a __name__, which is not the case anymore if the provided function is in fact a decorated method, even when functools.wraps is used in the decorator.
  • Joël
    Joël over 3 years
    And as a result, +1 for @Fabiano: using update_wrapper instead of @wraps does the job :)
  • Vijay Anand Pandian
    Vijay Anand Pandian over 2 years
    Thanks for the reference