search iconsearch icon
Type something to search...

Python decorators

Python decorators

0. Intro

Decorators are a really useful tool for python development but not a lot of people know what they are and how they work. In this post you are going to master them so that you can use them and create your own.

1. Understanding functions

Before starting with decorators it is important to understand how functions works in python. Functions have parameters for example:

def hello_person(name):
    """ Says hello to someone """
    return "hello {}".format(name)

This takes name as a parameter. It is even possible to define functions inside functions and children function would be able to use their parent parameters.

def message_to_person(message, name):
    def send_message():
        """ Sends message """
        return message
    
    return "{} {}".format(send_message(), name)

We can pass any object as a parameter to a function. We can even pass a function as a parameter.

2. Decorators basics

The idea behind decorators is to modify a function so that some things happen ā€˜aroundā€™ that function. This means before or after the function (or both).

For example letā€™s create a function that will create and html title by addind <h1> and </h1> around:

def decorate_h1(func):
    """ Decorates a function in order to add <h1> tags """
    
    def make_h1(text):
        return "<h1>{}</h1>".format(func(text))
    
    return make_h1
    
hello_person_h1 = decorate_h1(hello_person)
hello_person_h1("John")

> <h1>hello John</h1>

Right now we have ā€˜decoratedā€™ the hello_person function and save as the new function hello_person_h1. By calling hello_person_h1("John") the output is <h1>hello John</h1>.

We can see another example that will time the execution and the function and print with that time after the function is completed.

from time import time, sleep

def timeit(func):
    """ Timing decorator """
    
    def timed(*args):
        """ Prints the execution time of a function """
        t0 = time()
        result = func(*args)
        print("Done in {:.2f} seconds".format(time() - t0))    
        return result
    
    return timed

def wait_3_seconds():
    sleep(3)

wait_3_seconds_and_print = timeit(wait_3_seconds)
wait_3_seconds_and_print()

Done in 3.00 seconds

The *args parameter used at timed and func is a trick to preserve whatever parameter is passed to the original function.

Now we have the timeit decorator, the wait_3_seconds and the wait_3_seconds_and_print functions. Those both will wait 3 seconds but the second one will print the execution time.

3. Using real decorators

Now that we understand the idea behind decorators, we can rewrite the examples using real decorators. The behavior will be exactly the same.

@decorate_h1
def hello_person_with_h1(name):
    """ Says hello to someone """

    return "hello {}".format(name)

This is exactly the same as the function hello_person_h1 from the other example

@timeit
def wait_3_seconds_with_print():
    sleep(3)

And the same with the wait function. And the cool thing is that we can use both decorators at the same time.

@timeit
@decorate_h1
def wait_and_say_hello(name):
    """ Says hello to someone """
    sleep(3)
    return "hello {}".format(name)

wait_and_say_hello("Turtle")

Done in 3.00 seconds > <h1>hello Turtle</h1>

This will prints Done in 3.00 seconds and the output is <h1>hello Turtle</h1>

To sum up decorators allow to do things before or after (or both) a function:

def printer(func):
    """ Printer decorator """
    
    def prints():
        """ Prints some things """
        print("Before function")
        result = func()
        print("After function")    
        return result
    
    return prints

@printer
def hello():
    print("hello world")

hello()

Before function hello world After function

4. Passing arguments to decorators

It is possible to pass arguments to decorators. The idea is to create another wrapper that will take the parameters. For example:

def printer_with_params(before="Before function", after="After function"):
    """ Wrapper around decorator """
    
    def printer(func):
        """ Printer decorator """

        def prints():
            """ Prints some things """
            print(before)
            result = func()
            print(after)    
            return result

        return prints
    
    return printer

@printer_with_params(before="Argh!")
def hello_argh():
    print("hello world")

hello_argh()

Argh! hello world After function

This decorator allows the user to decide what to print before the main function.

5. Some cool examples

Now that we understand how decorators work, letā€™s see some useful examples of what decorators can offer.

5.1. supertimeit decorator

This is a decorator that takes one outputting function and outputs the execution time. By default it will use the print function.

import functools

def supertimeit(output_func=print):
    """ Allows to use custom output functions (like print, log.info...)"""

    def timeit_decorator(func):
        """ Timing decorator """
    
        @functools.wraps(func)
        def timed_execution(*args):
            """ Outputs the execution time of a function """
            t0 = time()
            result = func(*args)
            output_func("Done in {:.2f} seconds".format(time() - t0))    
            return result

        return timed_execution
    
    return timeit_decorator

By adding @functools.wraps(func) the documentation of the original function will be preserved

The default usage will be the same as the timeit decorator used before:

@supertimeit() # Since it can take params, we need the '()'
def wait_2_seconds_and_print():
    sleep(2)

wait_2_seconds_and_print()

Done in 2.00 seconds

The intersting part is that we can use other outputting functions instead of print, for example some logging:

import logging

@supertimeit(logging.warning)
def wait_2_seconds_and_log():
    sleep(2)

wait_2_seconds_and_log()

WARNING:root: Done in 2.00 seconds

Now the function logs the time.

5.2. infallible decorator

This decorator will prevent any function from crashing and it will show the details of any Exception occurred.

def infallible(output_func=print):
    """ Allows to use custom output functions (like print, log.info...)"""

    def infallible_decorator(func):
        """ Timing decorator """
    
        @functools.wraps(func)
        def infallible_execution(*args):
            """ Outputs the execution time of a function """
            
            try:
                return func(*args)
            except Exception as e:
                output_func("Error in '{}': {}".format(func.__name__, e))

        return infallible_execution
    
    return infallible_decorator

func.__name__ is a way to get the function name as a string

To see how it works letā€™s create a function that crashes:

@infallible()
def wrong_math(x):
    """ This divides a number by 0 """
    return x/0

wrong_math(1)

Error in ā€˜wrong_mathā€™: division by zero

Now we see a message when it crashes

5.3. Using them both

And the best part is that we can use supertimeit and infallible decorators together.

@supertimeit()
@infallible(logging.error)
def unuseful_function(x):
    """ It will wait some time and then it will divide a number by 0 """

    sleep(2)
    return x/0

unuseful_function(10)

ERROR:root: Error in ā€˜unuseful_functionā€™: division by zero

Done in 2.00 seconds

This logs ERROR:root:Error in 'unuseful_function': division by zero and print Done in 2.00 seconds.

After that post you should now understand python decorators and be able to create your own. If you have a really good idea for a decorator writte it down in the comments!