__wrapped__ in Python decorators

This is ano­ther of the new in­te­res­ting things that Py­thon 3 ha­s. Ti­me ago, it was so­me­how tri­cky to wo­rk wi­th de­co­ra­ted func­tion­s, be­cau­se the de­co­ra­to­r ­re­pla­ced the ori­gi­nal ob­jec­t, and its be­ha­viour be­ca­me hard to rea­ch.

So, for exam­ple, we all know that the fo­llo­wing sen­ten­ce:

@decorator
def function():
    pass

It’s ac­tua­lly syn­tax su­gar fo­r:

def function():
    pass

function = decorator(function)

So, if for so­me rea­so­n, we need to wo­rk bo­th wi­th the ori­gi­nal and de­co­ra­te­d ­func­tion­s, we re­qui­red tri­cks su­ch as di­ffe­rent na­me­s, so­me­thing like:

def _function():
    pass

function = decorator(_function)

But in current Python, this should be no longer the case. As we can see from the code of the functools module 1, when decorating an object, there is an attribute named __wrapped__ that holds the reference to the original one.

So now if we use this, we can ac­ce­ss it di­rec­tly wi­thout ha­ving to re­sort to­ ­the old qui­rks.

Example

Le­t’s con­si­der this exam­ple. Va­li­da­ting pa­ra­me­ter ty­pes like this is fa­r ­from a real im­ple­men­ta­tio­n, but ra­ther so­me­thing for the sake of i­llus­tra­tio­n.

from functools import wraps


def validate_parameters(kwargs, annotations):
    """For a dictionary of kwargs, and another dictionary of annotations,
    mapping each argument name with its type, validate if all types on the
    keyword arguments match those described by the annotations.
    """
    for arg, value in kwargs.items():
        expected_type = annotations[arg]
        if not isinstance(value, expected_type):
            raise TypeError(
                "{0}={1!r} is not of type {2!r}".format(
                    arg, value, expected_type)
                )


def validate_function_parameters(function):
    @wraps(function)
    def inner(**kwargs):
        annotations = function.__annotations__
        validate_parameters(kwargs, annotations)
        return function(**kwargs)
    return inner



@validate_function_parameters
def test_function(x: int, y:float):
    return x * y


test_function.__wrapped__(x=1, y=3)  # does not validate
test_function(x=1, y=3)  # validates and raises

The sta­te­ment in the pe­nul­ti­ma­te li­ne wo­rks be­cau­se it’s ac­tua­lly in­vo­king the o­ri­gi­nal func­tio­n, wi­thout the de­co­ra­tor applie­d, whe­reas the last one fail­s ­be­cau­se it’s ca­lling the func­tion al­ready de­co­ra­te­d.

Potential use cases

In ge­ne­ral this a ni­ce fea­tu­re, for the rea­son that in a way ena­bles ac­ce­s­sin­g ­bo­th ob­jec­ts, (it could be ar­gued that de­co­ra­ting a func­tion cau­ses so­me sor­t of tem­po­ral cou­plin­g).

Most im­por­tan­tl­y, it can be us­ed in unit tes­ts, whe­ther is to test the o­ri­gi­nal func­tio­n, or that the de­co­ra­tor itself is ac­ting as it is su­ppo­sed to­ ­do.

Moreover, we could tests our own code, before being decorated by other libraries being used in the project (for example the function of a tasks without the @celery.task decorator applied, or an event of the database of SQLAlchemy before it was changed by the listener event decorator, etc.).

Just ano­ther tri­ck to keep in the tool­bo­x.

1

This attri­bu­te is do­cu­men­te­d, but I first found about it whi­le rea­din­g at the co­de of CP­y­thon at http­s://­gi­thu­b.­co­m/­p­y­tho­n/­cp­y­tho­n/­blo­b/3405792­b024e9­c6­b70­c0­d2355­c55a23a­c84e1e67/­Li­b/­func­tool­s.­p­y#­L70