Python Decorators 101

Python Decorators 101

A lot has been written over time about these cheeky little creatures and I couldn't possibly shy away from throwing a bit of code together.

Before I start, what is a decorator? It's a little monkey that adds extra functionality to your function (or class).

Let's hit it. Create a file called decorator.py and copy and paste (or type) the below example:

import typing


def metadata(func: typing.Callable) -> typing.Callable:
    def call(*args, **kwargs):
        metadata_ = {
            "function_name": func.__name__,
            "docstring": func.__doc__
        }
        print(metadata_)
        return func(*args, **kwargs)
    return call
decorator.py

In this case it's hopefully obvious that this decorator (1) takes a function (callable) as an input (2) prints some metadata about the function passed and (3) outputs the very same function. Let's go ahead and decorate.

@metadata
def get_version() -> str:
    """ Returns your python interpreter version """
    return sys.version
decorator.py.

So this is how you decorate! That looks familiar right? Like @staticmethod or @property in a class or @login_required in django. Let's get into out REPL and execute get_version()

python -i decorator.py 
>>> get_version()
{'function_name': 'get_version', 'docstring': 'Returns your python interpreter version'}
'3.9.10 (main, Jan 29 2022, 05:27:31) \n[GCC 10.2.1 20210110]'

We can see the metadata at runtime but if we do the below:

>>> get_version.__name__
'call'
>>> get_version.__doc__
>>> 

What happened? __name__ should be get_version and the docstring should definitely not be empty.

functools to the rescue

Let's import wraps from functools and add it to our metadata decorator like this:

import typing
from functools import wraps


def metadata(func: typing.Callable) -> typing.Callable:
    @wraps(wrapped=func)
    def call(*args, **kwargs):
        metadata_ = {
            "function_name": func.__name__,
            "docstring": func.__doc__
        }
        print(metadata_)
        return func(*args, **kwargs)
    return call

Repeating the previous exercise we now see the metadata remains intact.

# python -i decorator.py 
>>> get_version.__name__
'get_version'
>>> get_version.__doc__
' Returns your python interpreter version '
>>> 

Here's a slightly more complex decorator example if you feel adventurous

Have a read about functools.wraps too if you wish to know more.

Enjoy coding!

コードをお楽しみください