Practical Python: Introduction to Decorators
By: Soner Yıldırım
Tags: Artificial Intelligence, Machine Learning, Programming, Python, Data Science

Practical Python: Introduction to Decorators

How and when to use decorators explained with examples

Photo by Markus Spiske on Unsplash

Functions in Python are first-class objects which means they can be used as arguments for other functions. Decorators arise from this notion.

A decorator is a function that takes another function and extends its behavior. The base function is not modified. The decorator wraps it and add additional functionality.

Let’s try to associate it with a real life scenario. Consider you run 400 meters for your daily exercise. You running 400 meters is a function. If you want to measure the time it takes for you to finish 400 meters, you record the time before you start and after you finish your run. The time measurement can be considered as another function that adds behavior to the run function. This is basically how decorators work.

You may argue that why we do not implement the timing inside the run function. First of all, functions should be focused on one particular task. It is is the more appropriate and efficient way of using functions.

In addition to that, you may need to use the timing decorator to decorate other functions as well. For instance, you may need to time a different exercise. Without the decorators, you would have to implement the timing inside all the exercise functions.

We now have an idea of what decorators are used for. Let’s start on doing the examples.

Here is a simple greeting function.

def greeting():

print("Hello there!") greeting()

Hello there!

This function prints a greeting message. We want to decorate this function in a way that we do something before and after its execution.

def my_decorator(f):

def wrapper():

print("Something is done before the function")

result = f()

print("Something is done after the function")

return result

return wrapper

The my_decorator function takes a function as argument and decorates it with additional behavior. In this case, the additional behavior is just printing something before and after executing the function.

In other words, my_decorator is a function that extends the behavior of another function without modifying it which makes it a decorator.

We can now wrap the greeting function with my_decorator.

decorated_greeting = my_decorator(greeting) decorated_greeting() Something is done before the function

Hello there!

Something is done after the function

We have just done a simple decorating. However, there is nicer and cleaner way of doing it.

@my_decorator

def greeting():

print("Hello there!") greeting()

Something is done before the function

Hello there!

Something is done after the function

We can just put the decorator function with “@” sign right before the definition of the function we want to wrap.

The additional behavior in my_decorator seems to be useless. Let’s create a useful decorator. For instance, we can wrap a function with decorator that takes the time it takes to execute the function.

import time def timing(f):

def wrapper():

start = time.time()

result = f()

print(f"The function took {time.time() - start} to execute")

return result

return wrapper

The timing is our decorator. In the next step, we will use it to wrap a function that multiplies three numbers.

@timing

def mult(a, b, c):

"""This function multiplies 3 numbers"""

return a*b*c

Let’s call the mult function:

mult(2, 3, 4) TypeError: wrapper() takes 0 positional arguments but 3 were given

We get a TypeError which is related to the function arguments. This is expected because the wrapper function does not have any idea about the arguments of the mult function. We can solve this problem by using *args and **kwargs as follows.

def timing(f):

def wrapper(*args, **kwargs):

start = time.time()

result = f(*args, **kwargs)

print(f"The function took {time.time() - start} to execute")

return result

return wrapper

The problem is solved. The mult function can be wrapped with the timing decorator now.

@timing

def mult(a, b, c):

"""This function multiplies 3 numbers"""

return a*b*c mult(2, 3, 4) The function took 2.1457672119140625e-06 to execute

60

Docstrings are informative texts that tell us what a function does. There are various ways to access or view the docstring of a function. The __doc__ method or help function can be used to view the docstring.

Let’s try it on the mult function we have just wrapped with the timing decorator.

help(mult) Help on function wrapper in module __main__:

wrapper(*args, **kwargs)

What help returns does not give us any particular information about the mult function. Instead, it returns the function used to wrap it which is called wrapper.

There is a built-in function of Python to overcome this issue which is the wraps function.