__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 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 state­ment in the penul­ti­mate line works be­cause it’s ac­tu­al­ly in­vok­ing the o­rig­i­nal func­tion, with­out the dec­o­ra­tor ap­plied, where­as the last one fail­s 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 o­rig­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