Decorators in Python with Examples

Introduction

In python, decorators are used to add some behavior to existing callable (functions, methods, and classes). Additional behavior is achieved by wrapping existing callable inside another function which adds some functionality to it. These wrappers are known as decorators in Python.

To understand decorators, we consider following use case which is often required while programming:

Use Case Example: Measuring Execution Time

Suppose there are different function defined in your program in order to accomplish some task. And you're asked to measure execution time - how long they execute - for each function when they are called.

By now, you must be thinking that - okay, no problem, will go to every functions and timeit them, right? That's all right when you have to do it for few functions. But if there are lot of functions then you are repeating that timeit thing for every function, right?

So, what do you do here? You write decorator function for measuring execution time and decorate every function using @decorator_name which we are going to explain next.

Example

Suppose we have following function in our program:


def function_1():
    for i in range(100000):
        pass

def function_2():
    for i in range(10000000):
        pass

def function_3():
    for i in range(10000):
        pass

def function_4():
    for i in range(100000000):
        pass

And we want to measure execution time of each function when they are called. To measure execution time we create decorator function called timer like this:


# Timer decorator

# Here timer is name of decorator
def timer(fn):
    from time import perf_counter
    
    def inner(*args, **kwargs):
        start_time = perf_counter()
        to_execute = fn(*args, **kwargs)
        end_time = perf_counter()
        execution_time = end_time - start_time
        print('{0} took {1:.8f}s to execute'.format(fn.__name__, execution_time))
        return to_execute
    
    return inner

Now we use @decorator_name to decorate any function for which we need to measure execution time like this:


# Timer decorator

def timer(fn):
    from time import perf_counter
    
    def inner(*args, **kwargs):
        start_time = perf_counter()
        to_execute = fn(*args, **kwargs)
        end_time = perf_counter()
        execution_time = end_time - start_time
        print('{0} took {1:.8f}s to execute'.format(fn.__name__, execution_time))
        return to_execute
    
    return inner

# Decorator in action
@timer
def function_1():
    for i in range(100000):
        pass

@timer
def function_2():
    for i in range(10000000):
        pass

@timer
def function_3():
    for i in range(10000):
        pass

@timer
def function_4():
    for i in range(100000000):
        pass


# Making function call 
function_1()
function_2()
function_3()
function_4()

Here is the output of the above program:

function_1 took 0.00620240s to execute
function_2 took 0.79993060s to execute
function_3 took 0.00093230s to execute
function_4 took 8.81120080s to execute

Explanation

When we decorate function like this:


@timer
def function_1():
    for i in range(100000):
        pass

Then this is equivalent to writing:


def function_1():
    for i in range(100000):
        pass

function_1 = timer(function_1)

Here, function_1 is passed to decorator function timer() as an argument and whatever it returns is stored in same name i.e. function_1 on the left hand side of statement function_1 = timer(function_1). So, remember function_1 on the right hand side of statement is not same as function_1 on the left hand side.

Now lets look at what function timer() actually returns.

When we call timer() we have passed function_1 which is received in variable fn in the scope of timer() function, which is actually free variable in closure or in inner() function.

And finally timer() function returns inner() function which is stored in variable function_1 on the left hand side of statement function_1 = timer(function_1). Thus obtained function_1 is said to be deorated by function timer().

So, what happens when we call decorated function_1?

When function_1() is called then it executes inner() function. This function adds extra functionality of measuring execution time and it finally returns fn stored in variable to_execute. So what is fn here? It is the original function function_1, right? Thus at the end actual function function_1() is executed doing its normal task.

And the process is same for other function as well.

Key Points:

  1. Decorator takes a function as an argument.
  2. Decorator retuns a Closure.
  3. Closure generally accepts any number of arguments through (*args, **kwargs).
  4. Decorators adds some additional functionality in Closure (inner function).
  5. Closure function calls the original function.