Decorators

Functions can also be passed as arguments to other functions and return the results of other functions. For example, it is possible to write a Python function that takes another function as a parameter, embeds it in another function that does something similar, and then returns the new function. This new combination can then be used instead of the original function:

 1 >>> def inf(func):
 2 ...     print("Information about", func.__name__)
 3 ...     def details(*args):
 4 ...         print("Execute function", func.__name__, "with the argument(s)")
 5 ...         return func(*args)
 6 ...     return details
 7 ...
 8 >>> def my_func(*params):
 9 ...     print(params)
10 ...
11 >>> my_func = inf(my_func)
12 Information about my_func
13 >>> my_func("Hello", "Pythonistas!")
14 Execute function my_func with the argument(s)
15 ('Hello', 'Pythonistas!')
Line 2

The inf function outputs the name of the function it wraps.

Line 6

When finished, the inf function returns the wrapped function.

A decorator is syntactic sugar for this process and allows you to wrap one function inside another with a one-line addition. You still get exactly the same effect as with the previous code, but the resulting code is much cleaner and easier to read. Using a decorator simply consists of two parts:

  1. the definition of the function to wrap or decorate other functions, and

  2. the use of an @ followed by the decorator just before the wrapped function is defined.

The decorator function should take a function as a parameter and return a function, as follows:

1 >>> @inf
2 ... def my_func(*params):
3 ...     print(params)
4 ...
5 Information about my_func
6 >>> my_func("Hello", "Pythonistas!")
7 Execute function my_func with the argument(s)
8 ('Hello', 'Pythonistas!')
Line 1

The function my_func is decorated with @inf.

Line 7

The wrapped function is called after the decorator function is finished.

functools

The Python functools module is intended for higher-order functions, for example functions that act on or return other functions. Mostly you can use them as decorators, such as:

functools.cache()

Simple, lightweight, function cache as of Python ≥ 3.9, sometimes called memoize. It returns the same as functools.lru_cache() with the parameter maxsize=None, additionally creating a Dictionaries with the function arguments. Since old values never need to be deleted, this function is then also smaller and faster. Example:

1>>> from functools import cache
2>>> @cache
3... def factorial(n):
4...     return n * factorial(n - 1) if n else 1
5...
6>>> factorial(8)
740320
8>>> factorial(10)
93628800
Line 6

Since there is no previously stored result, nine recursive calls are made.

Line 8

makes only two new calls, as the other results come from the cache.

functools.wraps()

This decorator makes the wrapper function look like the original function with its name and properties.

>>> from functools import wraps
>>> def my_decorator(f):
...     @wraps(f)
...     def wrapper(*args, **kwargs):
...         """Wrapper docstring"""
...         print("Call decorated function")
...         return f(*args, **kwargs)
...     return wrapper
...
>>> @my_decorator
... def example():
...     """Example docstring"""
...     print("Call example function")
...
>>> example.__name__
'example'
>>> example.__doc__
'Example docstring'

Without @wraps decorator, the name and docstring of the wrapper method would have been returned instead:

>>> example.__name__
'wrapper'
>>> example.__doc__
'Wrapper docstring'