Preserving data about the original wrapped object

One of the most common problems when applying a decorator to a function is that some of the properties or attributes of the original function are not maintained, leading to undesired, and hard-to-track, side-effects.

To illustrate this we show a decorator that is in charge of logging when the function is about to run:

# decorator_wraps_1.py

def trace_decorator(function):
def wrapped(*args, **kwargs):
logger.info("running %s", function.__qualname__)
return function(*args, **kwargs)

return wrapped

Now, let's imagine we have a function with this decorator applied to it. We might initially think that nothing of that function is modified with respect to its original definition:

@trace_decorator
def process_account(account_id):
"""Process an account by Id."""
logger.info("processing account %s", account_id)
...

But maybe there are changes.

The decorator is not supposed to alter anything from the original function, but, as it turns out since it contains a flaw it's actually modifying its name and docstring, among other properties.

Let's try to get help for this function:

>>> help(process_account)
Help on function wrapped in module decorator_wraps_1:

wrapped(*args, **kwargs)

And let's check how it's called:

>>> process_account.__qualname__
'trace_decorator.<locals>.wrapped'

We can see that, since the decorator is actually changing the original function for a new one (called wrapped), what we actually see are the properties of this function instead of those from the original function.

If we apply a decorator like this one to multiple functions, all with different names, they will all end up being called wrapped, which is a major concern (for example, if we want to log or trace the function, this will make debugging even harder).

Another problem is that, in case we placed docstrings with tests on these functions, they will be overridden by those of the decorator. As a result, the docstrings with the test we want will not run when we call our code with the doctest module (as we have seen in Chapter 1, Introduction, Code Formatting, and Tools).

The fix is simple, though. We just have to apply the wraps decorator in the internal function (wrapped), telling it that it is actually wrapping function:

# decorator_wraps_2.py
def trace_decorator(function):
@wraps(function)
def wrapped(*args, **kwargs):
logger.info("running %s", function.__qualname__)
return function(*args, **kwargs)

return wrapped

Now, if we check the properties, we will obtain what we expected in the first place. Check help for the function, like so:

>>> Help on function process_account in module decorator_wraps_2:

process_account(account_id)
Process an account by Id.

And verify that its qualified name is correct, like so:

>>> process_account.__qualname__
'process_account'

Most importantly, we recovered the unit tests we might have had on the docstrings! By using the wraps decorator, we can also access the original, unmodified function under the __wrapped__ attribute. Although it should not be used in production, it might come in handy in some unit tests when we want to check the unmodified version of the function.

In general, for simple decorators, the way we would use functools.wraps would typically follow the general formula or structure:

def decorator(original_function):
@wraps(original_function)
def decorated_function(*args, **kwargs):
# modifications done by the decorator ...
return original_function(*args, **kwargs)

return decorated_function

Always use functools.wraps applied over the wrapped function, when creating a decorator, as shown in the preceding formula.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.114.132