Python Decorator, Step by step introduction

Python Decorator, Step by step introduction

·

4 min read

What is Decorator in Python and what problem decorators will try to solve.

Let me explain with an example.

Here we have 2 functions , calculate square and calculate cube.

def calc_square(numbers):
    result = []
    for number in numbers:
        result.append(number*number)
    return result


def calc_cube(numbers):
    result = []
    for number in numbers:
        result.append(number*number*number)
    return result

array = range(1,100000)
out_square = calc_square(array)
out_cube = calc_cube(array)

Here, calc_square function takes array of numbers as an input and iterating through the array ,calculating the square of a number and putting that into a result.

calc_cube does same thing, but instead of square ,it does cube. Here we are calling those function for a range of 1 to 100000.

Now often you have a need of measuring the performance of a function. Performance, I mean how much time does every function take to execute. In order to measure the timing, you have to use the time module. I will take the start time . Once you are done ,you will take end time. The code snippet is shown below.

import time

def calc_square(numbers):
    start = time.time()
    result = []
    for number in numbers:
        result.append(number*number)
    end = time.time()
    print(" calc_square took " + str((end-start)*1000) + " mil sec ")
    return result

We do the same thing for cube function as well, because we want to measure the performance of both of these functions .

def calc_cube(numbers):
    start = time.time()
    result = []
    for number in numbers:
        result.append(number*number*number)
    end = time.time()
    print(" calc_cube took " + str((end-start)*1000) + " mil sec ")
    return result

The output of the above code will be

calc_square took 27.922630310058594 mil sec 
calc_cube took 41.88823699951172 mil sec

Now the problem with this code is that , lets say you have a complex software project and you have written more than 100 functions. In order to measure the performance of all those functions , you have to write the start time and end time , exactly the same line of code in every function .

Problem No.1 is The start time and end time is getting repeated in every function that you want to measure the performance.

Problem no.2 is , there is a logic in the function(calculating square and cube) and it is combined with the timing logic .It makes code less readable.

Untitled.jpg

Now there has to be a better way of doing this and better way is basically a Decorator.

Decorator allows you to wrap your function in another function .
Lets remove timing logic or timing code from above functions. And I want to have a function which has just the logic that function is supposed to do.

In order to do decorator , you need to define the wrapper function and lets call it as time_it(func) and that wrapper function will take function (func) as an argument.

Now functions are 1st class objects in Python means:

• You can pass functions as an argument to function.

• Also, you can return function as a return value from another function.

So in time_it(func) : we will define another function called wrapper (Python allows us to write nested function i. e we can write one function inside another function) and what wrapper function is doing is it is taking the positional arguments *args and keyword arguments which is **kwargs

And then it will start the timer and then it will call the function that was passed as an argument (So I am going to call function here) with argument and keyword argument

Then I will measure the end time.

The print(func.__name__ + “ took ” + str((end-start)**1000 + “ mil sec ”)

then return result

Then return wrapper function. i.e return wrapper

 __name__

will return the name of that function. Here we are returning a function (wrapper) from another function time_it , that’s why this function wrapper is called 1st class object . You can treat it as normal variable, you can return it and pass it as a function argument and so on.

def time_it(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args,**kwargs)
        end = time.time()
        print(func.__name__ + " took " + str((end-start)*1000) + " mil sec ")
        return result
    return wrapper

Also we need to decorate the calc_square and calc_cube function i.e @time_it which is shown below

@time_it
def calc_square(numbers):
    result = []
    for number in numbers:
        result.append(number*number)
    return result

@time_it
def calc_cube(numbers):
    result = []
    for number in numbers:
        result.append(number*number*number)
    return result

So, any function that you want to measure the performance of now, once you have defined this time_it function , you can put the @time_it tag the beginning and it is going to measure the performance . It makes both the function calc_square and calc_cube more readable and all your timing code is restricted into one function.

Final thoughts :

Decorator acts as a wrapper to the original function.