Python in Detail: functools.wraps
Welcome to the functools.wraps lesson!
This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!
Most decorators build and return new functions. There's a subtle problem with that: what happens to the original function's attributes, like
.__name__?>
def divide(a, b):return a / bdivide.__name__Result:
'divide'
If our decorator returns a function defined with
def wrapped, then the final decorated function's.__name__is"wrapped". We lose the original function's name.Here's the
silence_exceptionsdecorator from some previous lessons.>
def silence_exceptions(func):def wrapped(*args, **kwargs):try:return func(*args, **kwargs)except Exception:return Nonereturn wrapped- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
silence_exceptionsdef divide(a, b):return a / bdivide.__name__Result:
'wrapped'
Imagine a system where we decorate hundreds of functions. They all get the name "wrapped". In our generated documentation and logs, they all show up as "wrapped" instead of their real names! We can't tell which function is which.
We can manually fix the name when we decorate the function. In this example, note the new
wrapped.__name__ =line.>
def silence_exceptions(func):def wrapped(*args, **kwargs):try:return func(*args, **kwargs)except Exception:return Nonewrapped.__name__ = func.__name__return wrapped- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
silence_exceptionsdef divide(a, b):return a / bdivide.__name__Result:
'divide'
That works, but there's a problem with it:
.__name__isn't the only attribute that we need to copy! We also need to retain.__doc__, the function's docstring. There are more esoteric attributes to copy as well:.__qualname__,.__module__,.__annotations__, and.__type_params__. Future versions of Python may add even more attributes that we should copy.Think for a moment about the shape of this problem: "we need to change many decorator functions in the same way". Python already has a way to change many functions in the same way: decorators!
The built-in
functools.wrapsdecorator copies all of these special attributes from the original function to the decorated wrapper. It's a decorator that we use on decorators! If future versions of Python add more attributes that we should copy,functools.wrapswill copy those as well.Pay close attention to the
@functools.wraps(func)line in the next example.>
import functoolsdef silence_exceptions(func):functools.wraps(func)def wrapped(*args, **kwargs):try:return func(*args, **kwargs)except Exception:return Nonereturn wrapped- Note: this code example reuses elements (variables, etc.) defined in earlier examples.
>
silence_exceptionsdef divide(a, b):return a / bdivide.__name__Result:
'divide'
You might wonder: shouldn't Python do this for us automatically? It knows that we're decorating a function, right?
No, it doesn't know that! Decorators usually return functions. But they sometimes return other values too, as we saw in an earlier lesson. For example, here's a decorator that returns integers.
>
def replace_with_55(f):return 55replace_with_55def divide(x, y):return x / ydivideResult:
55
The
@some_decoratorsyntax does two things. First, it callssome_decoratoron the decorated value. Second, it replaces the original value with the decorator's return value.Python can't make any assumptions beyond those two things. If we want anything extra, like copying a function's
.__name__, then we have to ask for it.