__wrapped__ in Python decorators

This is an­oth­er of the new in­ter­est­ing things that Python 3 has. Time ago, it was some­how tricky to work with dec­o­rat­ed func­tion­s, be­cause the dec­o­ra­tor re­placed the orig­i­nal ob­jec­t, and its be­hav­iour be­came hard to reach.

So, for ex­am­ple, we all know that the fol­low­ing sen­tence:

@decorator
def function():
    pass

It’s ac­tu­al­ly syn­tax sug­ar for:

def function():
    pass

function = decorator(function)

So, if for some rea­son, we need to work both with the orig­i­nal and dec­o­rat­ed func­tion­s, we re­quired tricks such as dif­fer­ent names, some­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­cess it di­rect­ly with­out hav­ing to re­sort to the old quirk­s.

Example

Let’s con­sid­er this ex­am­ple. Val­i­dat­ing pa­ram­e­ter types like this is far from a re­al im­ple­men­ta­tion, but rather some­thing for the sake of il­lus­tra­tion.

from func­tools im­port wraps
def val­i­date_­pa­ram­e­ters(kwargs, an­no­ta­tions):
    """­For a dic­tio­nary of kwargs, and an­oth­er dic­tio­nary of an­no­ta­tion­s,
    map­ping each ar­gu­ment name with its type, val­i­date if all types on the
    key­word ar­gu­ments match those de­scribed by the an­no­ta­tion­s.
    """
    for arg, val­ue in kwargs.items():
        ex­pect­ed_­type = an­no­ta­tions[arg]
        if not isin­stance(val­ue, ex­pect­ed_­type):
            raise Type­Er­ror(
                "{0}={1!r} is not of type {2!r}".for­mat(
                    arg, val­ue, ex­pect­ed_­type)
                )
def val­i­date_­func­tion_­pa­ram­e­ters(func­tion):
    @wraps(func­tion)
    def in­ner(**kwargs):
        an­no­ta­tions = func­tion.__an­no­ta­tion­s__
        val­i­date_­pa­ram­e­ters(kwargs, an­no­ta­tions)
        re­turn func­tion(**kwargs)
    re­turn in­ner
@val­i­date_­func­tion_­pa­ram­e­ters
def test_­func­tion(x: int, y:float):
    re­turn x * y
test_­func­tion.__wrapped__(x=1, y=3)    # does not val­i­date
test_­func­tion(x=1, y=3)    # val­i­dates and rais­es

The state­ment in the penul­ti­mate line works be­cause it’s ac­tu­al­ly in­vok­ing the orig­i­nal func­tion, with­out the dec­o­ra­tor ap­plied, where­as the last one fails be­cause it’s call­ing the func­tion al­ready dec­o­rat­ed.

Potential use cases

In gen­er­al this a nice fea­ture, for the rea­son that in a way en­ables ac­cess­ing both ob­ject­s, (it could be ar­gued that dec­o­rat­ing a func­tion caus­es some sort of tem­po­ral cou­pling).

Most im­por­tant­ly, it can be used in unit tests, whether is to test the orig­i­nal func­tion, or that the dec­o­ra­tor it­self is act­ing as it is sup­posed 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 an­oth­er trick to keep in the tool­box.

1

This at­tribute is doc­u­ment­ed, but I first found about it while read­ing at the code of CPython at http­s://github.­com/python/cpython/blob/3405792b024e9c6b70c0d2355c55a23ac84e1e67/Lib/­func­tool­s.py#L70