Python decorators
- 2019-04-16
- š Python
- Python
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!