Why do I need functools.wraps?

Decorators are a useful part of Python. They allow one to adjust functions in a similar, predictable manner and can be especially powerful due to their concise expressiveness when utilized by certain frameworks, especially web frameworks like Flask.

That being said, one issue with decorators is that they copy the attributes of the functions they wrap. An example will make this more clear.

Example without functools.wraps

The following is taken from the Python3 functools documentation, but has the @wraps(f) section removed.

def my_decorator(f):
    def wrapper(*args, **kwds):
        print('Calling decorated function')
        return f(*args, **kwds)
    return wrapper

@my_decorator
def example():
    """Docstring"""
    print('Called example function')

example()
>>> Calling decorated function
>>> Called example function

As you can see, the above works as expected. However, if we inspect some of the attributes from the function, we see that some attributes have been overwritten.

example.__name__
>>> 'wrapper'

example.__doc__
>>> 

The function has lost its name of 'example' and has been replaced by wrapper. Similarly, the non-existent docstring from wrapper has overwritten the docstring in example.

Example with functools.wraps

Now let's run the same code above, use the wraps decorator:

from functools import wraps

def my_decorator(f):
    @wraps(f)
    def wrapper(*args, **kwds):
        print('Calling decorated function')
        return f(*args, **kwds)
    return wrapper

@my_decorator
def example():
    """Docstring"""
    print('Called example function')

example()
>>> Calling decorated function
>>> Called example function

The attributes we inspected above now return what would be expected for the example function.

example.__name__
>>> 'example'

example.__doc__
>>> 'Docstring'

The following are the attributes that are 'retained' (more correctly, 'copied over' to wrapper, but this makes more sense colloquially):

functools.WRAPPER_ASSIGNMENTS
>>> ('__module__', '__name__', '__qualname__', '__doc__', '__annotations__')

Why does this matter?

The above situation might not seem like a big deal, and in most cases it probably isn't. However, I'm sure people can think of situations where a program's logic could depend on what's available in the __name__ attribute. If various functions are wrapped with the same decorator, they would all get the name of the internal wrapper function, which would definitely not be desired if logic is based on __name__.

Access to the original function

Another nice aspect of wraps is that it adds a __wrapped__() attribute to the wrapped function that allows access to the original function if introspection is necessary. The example function that is called is technically calling wrapper (which also happens to call example in its definition). If we wanted to work with example like it was never wrapped, we would work with example.__wrapped__().

Now what about functools.update_wrapper?

update_wrapper is what actually performs the work, but wraps is cleaner to invoke as a wrapper. They're equivalent in that wraps is merely a partial function of update_wrapper that has its parameters frozen. Long story short, you should be fine just using wraps, but should be aware of update_wrapper in case you need more control over the wrapped function.

Summary

The wraps decorator is an easy way to retain attributes for a wrapped function instead of inheriting from the wrapper function definition. By using this decorator you can ensure reliability and consistency of your function attributes.

Comments

comments powered by Disqus

Published

Category

Python

Tags

Contact