Skip to content

decorators

Decorators are wrapper functions that enhance the wrapped function. It does not change the behaviour of the original function, but instead wrap it with some new behavior, achieving extensibility and composability.

A decorator for the same function looks like this:

def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)  # call the function
        total_time = time.time() - start_time
        print(f'execution time: {total_time}')
        return result
    return wrapper

@timer
def slow_func(a, b):
    time.sleep(1)
    return a * b

slow(5,6)

# execution time: 1.001169204711914
# 30

Decorators are however not the only way to transform a function. The same could be achieved like this

slow = timer(slow)
slow(5,6)

The decorator pattern can be seen as syntactic sugar for the function above.

template

In basis the template for a wrapper is this:

def decorator(func):

    def wrapper(*args, **kwargs):
        # something before
        result = func(*args, **kwargs)
        # something after
        return result

    return wrapper
decorator arguments

What if you want to change the output of the decorator function timer? For example, only show the number instead of a pretty text.

Remember that there is also a way to write it, without the decorator:

slow = timer(prettyprint=False)(slow)
slow(5,6)

From that, it follows that the decorator template would then look like this:

def timer(prettyprint=True):

    def wrapped(func):

        def wrapper(*args, **kwargs)
            # code
            return func(*args, **kwargs)

        return wrapper

    return wrapper

The first line receives expects the argument as given in the decorator @timer(prettyprint=False), then wrapped gets the actual function, and in turn calls wrapper() with arguments.

decorate class

Classes or methods in a class can have decorators as well. This example adds a reusable function that logs something each time the class is instantiated

import logging
import time
logging.basicConfig(filename='example.log', encoding='utf-8', level=logging.DEBUG)

def logger(cls):
    # do something with class cls here

    def wrapper(*args, **kwargs):
        logging.info(f'{time.time()} {cls.__class__.__name__} ')

        obj = cls(*args, **kwargs)
        # do something with the object here
        return obj

    return wrapper

@logger
class A:

    def __init__(self, x, y):
        self.x = x
        self.y = y

objs = [A(x=1, y=2) for _ in range(10)]
decorate a class

Classes can be decorated as well and it gives us some re-usable code that can change the behaviour of a class. It could be used to add or overwrite attributes of classes. The @dataclass decorator is a good example of this.

In this example we will replace the __repr__ method:

def different_repr(cls):

    cls.__repr__ = new_repr  # a new function

    def wrapper(*args, **kwargs):
        return cls(*args, **kwargs)

    return wrapper
functools.wraps

When you use a decorator, you're replacing one function with another. Unfortunately this means when checking __name__, it will print the name of the decorator because that's the name of your new function. functools.wraps takes a function used in a decorator and adds the functionality of copying over the function name, docstring, arguments list, etc.

To complete the timer decorator from the first example, we should add @wraps:

from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)  # call the function
        total_time = time.time() - start_time
        print(f'execution time: {total_time}')
        return result
    return wrapper