Decorators are a beautiful, but slightly mind-bending concept. For the sake of having it written down once (primarily for myself), I decided to create this little tutorial. It’s purpose is by no means to cover all aspects of decorators, but rather to just introduce the idea and the Python syntax itself.

Decorators in Python, similarly to the decorator pattern, are used to modify or wrap callable objects such as functions. If you have ever used one of the larger web frameworks such as Flask or Django, you probably have already encountered them.

Decorators in Python

We need to know (well, at least it helps) three things about Python beforehand. First, in Python a function is an (first-class) object. Second, we should keep in mind that there are clearly defined local and global contexts. Third, functions can be nested, for example to create closures.

A decorator function in Python usually has at least one inner (local) function that handled the actual decoration process. In this sense, a decorator is a callable that returns a callable, e.g. a function or method.

To add a decorator to a function, we simple put @decorator_function_name before the function (callable object) that we want to decorate.

A Very Simple Example

In this example, we will create a decorator function (a_simple_decorator) which adds some text to a decorated function.

!A very simple code example.

We decorated the function hello_world. Now, on calling it, it will return Decoration! followed by Hello World!.

There is nothing there that doesn’t need to be there. You should also realize that we don’t return any values from the actual function and just call func(). In the following example we will address this issue.

Tracking Runtimes - More Realistic Example

While usually we would probably use timeit for this task, we will implement a decorator that allows to measure the runtime of any callable.

import time


def timing_decorator(func):
    """A decorator that times a callable."""

    def timing(*args, **kwargs):
        """The actual timer."""
        t_start = time.time()
        result = func(*args, **kwargs)
        t_end = time.time()

        print ('Runtime of: {} ({}, {}) {}s'.format(func.__name__, args,
                                                     kwargs, t_end - t_start))
        return result

    return timing

This decorator will take in a function (callable) with all its arguments (args) and keyword arguments (kwargs). It will then execute this function and return the time it has taken to run it.

In order to test this, we’ll be using a number of sorting algorithms provided by the beautiful pygorithm module.

import time
import random
from pygorithm.sorting import bubble_sort
from pygorithm.sorting import insertion_sort
from pygorithm.sorting import quick_sort


def timing_decorator(func):
    """A decorator that times a callable."""

    def timing(*args, **kwargs):
        """The actual timer."""
        t_start = time.time()
        result = func(*args, **kwargs)
        t_end = time.time()

        print ('Runtime of: {} ({}, {}) {}s'.format(func.__name__, args,
                                                     kwargs, t_end - t_start))
        return result

    return timing


@timing_decorator
def own_bubble_sort(unsorted_list):
    print(bubble_sort.sort(unsorted_list))


@timing_decorator
def own_insertion_sort(unsorted_list):
    print(insertion_sort.sort(unsorted_list))


@timing_decorator
def own_quick_sort(unsorted_list):
    print(quick_sort.sort(unsorted_list))

random.seed(42)
unsorted_list = [random.random() for _ in range(10000)]
own_bubble_sort(unsorted_list)
own_insertion_sort(unsorted_list)
own_quick_sort(unsorted_list)

The idea here is to use the decorator to test the speed of three sorting algorithms. For readability, I wrapped the three pygorithm functions into little wrappers of their own and then decorated them. A list of 10000 randomly generated numbers serve as the data for the sorting algorithms.

For those curious:

  • Bubble Sort: 9.3293297290802s
  • Insertion Sort: 0.00699162483215332s
  • Quick Sort: 0.023568391799926758s

In this case, insertion sort won the race.

When to Use Them?

There are many use cases (beyond the ones used in web frameworks) for decorators. There is a nice, and well structured, list of cases in the Python Wiki.

Some (further) ideas:

  • Require a login (authentication)
  • Debugging a function
  • Logging
  • Caching
  • Showing (deprecation) warnings
  • Synchronizing tasks
  • Check parameters and/or return values

Generally speaking, decorators are often useful as ‘throw-ins’ to enhance/modify new functions and as tools (e.g. for debugging purposes) provided by external modules or libraries.