__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:

def function():

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

def function():

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():

function = decorator(_function)

But in cu­rrent Py­tho­n, this should be no lon­ger the ca­se. As we can see fro­m ­the co­de of the func­tools mo­du­le [1], when de­co­ra­ting an ob­jec­t, the­re is an attri­bu­te na­med __w­ra­ppe­d__ that hol­ds the re­fe­ren­ce to the ori­gi­na­l o­ne.

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.


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):
    def inner(**kwargs):
        annotations = function.__annotations__
        validate_parameters(kwargs, annotations)
        return function(**kwargs)
    return inner

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.

Mo­reo­ve­r, we could tes­ts our own co­de, be­fo­re being de­co­ra­ted by othe­r ­li­bra­ries being us­ed in the pro­ject (for exam­ple the func­tion of a ta­sks wi­thout the @ce­le­r­y.­ta­sk de­co­ra­tor applie­d, or an event of the da­ta­ba­se of SQ­LAl­che­my be­fo­re it was chan­ged by the lis­te­ner event de­co­ra­to­r, etc.).

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

[1] This attribute is documented, but I first found about it while reading at the code of CPython at https://github.com/python/cpython/blob/3405792b024e9c6b70c0d2355c55a23ac84e1e67/Lib/functools.py#L70