Python Decorators


August 23, 2021, Learn eTutorial
1391

In this tutorial you will learn what python decorators are  and how to create and use them with the aid of simple examples. Before starting this tutorial let me reveal the fact that decorators are hard to understand! But we promise you that at the end you will master this topic unquestionably.

What are  decorators in python

Decorators are part of metaprogramming because they enhance the python code by adding extra functionality to the existing functions or classes at compile time.

A decorator can be defined as a function which takes another function and modifies  the behaviour of later function in one way or another way without explicitly modifying the actual source code.

Before starting this tutorial let me suggest you to cognize with basic topics of
functional programming such that you can grasp the concept of decorators without any hassle. A brief idea on these basics are provided below.

Concept of objects in python:

Python is a beautiful language which utilises the concept of objects extensively. In python , literally everything is an object. Variables, constants , function and even class is an object. Following example shows how a function works as an object.

Example : Function as object

def func1():
    print('Welcome to Learn eTutorials')

func1()

func2= func1
func2() 

Output:

Welcome to Learn eTutorials
Welcome to Learn eTutorials

When the above code gets executed, both func1 and func2 produce the same output. This indicates that func1 and func2 refer to the same function object.

Nested function:

Python allows the defining of a function inside another function , generally termed as nested function or inner function. Following is a simple example of nested function.

def OuterFunction(msg): 

                def Innerfunction():  
                                print(msg)
                InnerFunction() 
           
OuterFunction('Welcome to Learn eTutorials')  

Passing function as an argument to another function:

Functions are first class objects in python. What that means is a function can be passed as arguments, assigned to variables or can be used as return values, just like other objects(string ,list ,tuple etc) in python.

Example : Function as argument

def say_hello(name):
    print('Hello',name )

def say_bye(name):
    print('Bye',name )
    

def Greet(func, name):
    return func(name)

Greet(say_hello,'TOM')
Greet(say_bye,'JERRY') 

Output:

Hello TOM
Bye JERRY

In the above code ,say_hello and say_bye are two regular functions which take a string(name) as their argument. On the other hand,the Greet() function takes two parameters , one is a function and other is a string variable. Among all functions defined, the Greet() is a higher order function because it takes another function as its argument.

Returning functions from another function

In python it is possible to return a function from another function. In such situations function is regarded as return value. The following example returns the InnerFunction from the OuterFunction.

Closure Strucuture

Closure Strucuture

Here we are returning the InnerFunction without parentheses. This denotes that we are returning a reference to the InnerFunction which makes a closure. Checkout our previous tutorial to learn more about closures.

Above mentioned four features of functions make decorator possible in python.

Writing your first decorator

Now let's start with a simple example and try to understand the program.

Example: First decorator program

def decor_func(func):
    def wrap_func():
        print( '<' * 32)
        func()
        print('>' * 32)
    return wrap_func

def welcome():
    print('\nWELCOME ALL TO LEARN ETUTORIALS\n')

greet=decor_func(welcome)
greet() 

Explanation:

In the above example we have a regular function named welcome which takes no arguments and the purpose of the function is to print the given message “WELCOME ALL TO LEARN ETUTORIALS”.
Now suppose you wish to decorate the function welcome()  meantime you don't want to modify the source code. Is it possible ? The answer is yes. Decorators help you to decorate a function without touching the source code.

In order to decorate the welcome() ,we have defined a decorator function decor_func which takes an argument and the argument is a function. Inside the decor_func() we have again defined another function and name it as  wrap_func which takes no argument. The wrap_ func() calls the function which is passed as the argument in the decor_func and places it (func) in between extra functionalities that aim the modification . In our case we have included print() before and after the func . And finally return the wrap function , thus becoming a closure.

The so-called decoration occurs in the below statement

greet = decor_func(welcome) 

The function welcome() got decorated and the returned function is assigned to a name variable greet. The greet() , function call will produces the output and the output will be something like this:

Output:

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

WELCOME ALL TO LEARN ETUTORIALS

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

From the output it is pretty clear that we have decorated the function just like packing a gift. Here the decorator acts purely as a wrapper to the original function maintaining its nature as it is.Thus enhanced the beauty of the function.

Syntactic Sugar

Syntactic sugar in computer science is syntax in the programming language that makes the code more and sweeter to use. The syntactic way to declare a decorator in python is with the by using a @ symbol . The above code can be changed as follows :

Example: Syntactic Sugar

def decor_func(func):
    def wrap_func():
        print( '<' * 32)
        func()
        print('>' * 32)
    return wrap_func

@decor_func
def welcome():
    print('\nWELCOME ALL TO LEARN ETUTORIALS\n')

welcome() 

With the use of such syntactic sugars we can express the codes more clearly and elegantly. Here ,

@decor_func 

is equivalent to:

greet=decor_func(welcome)
greet() 

Multiple decorators on a function

Python allows the chaining of decorators enabling the ability of using multiple decorators on a single function. Only thing you need to bear in mind is the order of decorators you wish to apply on the function. Examine the following example to get a citing on stacked decorators.

#Stacked Decorators
def decor_func1(func):
    def wrap_func():
        print( '<' * 32)
        func()
        print('>' * 32)
    return wrap_func

def decor_func2(func):
    def wrap_func():
        print( 'XO' * 16)
        func()
        print('XO' * 16)
    return wrap_func

@decor_func1
@decor_func2
def welcome():
    print('\nWELCOME ALL TO LEARN ETUTORIALS\n')

#greet = decor_func(welcome)
#greet()
welcome() 

Output:

<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
XOXOXOXOXOXOXOXOXOXOXOXOXOXOXOXO

WELCOME ALL TO LEARN ETUTORIALS

XOXOXOXOXOXOXOXOXOXOXOXOXOXOXOXO
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

The above program contains two decorator functions namely, decor_func1 and decor_func2. These decorator functions are stacked in a specific order and hence follows the pattern in the output. Initially we decorate function welcome with decor_func2 and then with decor_func1. Now if we change the order of decor function the output will be different . Observe the output of the below program.

#Stacked Decorators
def decor_func1(func):
    def wrap_func():
        print( '<' * 32)
        func()
        print('>' * 32)
    return wrap_func

def decor_func2(func):
    def wrap_func():
        print( 'XO' * 16)
        func()
        print('XO' * 16)
    return wrap_func

@decor_func2
@decor_func1
def welcome():
    print('\nWELCOME ALL TO LEARN ETUTORIALS\n')

#greet = decor_func(welcome)
#greet()
welcome() 

Output:

XOXOXOXOXOXOXOXOXOXOXOXOXOXOXOXO
<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

WELCOME ALL TO LEARN ETUTORIALS

>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
XOXOXOXOXOXOXOXOXOXOXOXOXOXOXOXO

So from this we can conclude that the order of decorator function matters while stacking.

Since decorators are similar to an ordinary function we can imply all the features of a function on decorators too. Decorators can be used in other functions and even in other files simply by importing them. Hence, like functions, decorators are also reusable.

Decorating functions with arguments

When you observe the programs so far, you can notice that the  inner functions were kept empty. This means no arguments were passed to the inner functions. However at some point of time passing of arguments may be needed. Following is an example that divides two numbers and we apply decorators on it  considering the possibility of division by zero.

def check(func):
    def inner(x,y):
        print("Divide" ,x ,"by" ,y)
        if y == 0:
            print("Error: Division by zero is not allowed")
            return 
        return x / y
    return inner

@check
def division(a,b):
    return a/b

print(division(10,2)) 

Output:

Divide 10 by 2
5.0

Here, in this example the decorator function is check which takes the function  as its argument. The inner function also takes two variables x and y . The decorator function checks whether  variable y , which is the denominator part of the division is zero or not. If it is not equivalent to zero ,the output will be as above otherwise the output will be as follows:

Divide 10 by 0
Error: Division by zero is not allowed
None

So here the decorator check we created suits best for the division function only. However we know that decorators are not limited to any single function; it can be used by other functions also. A simple example of generic decorator to test the execution time is shown below:

from time import time
def timetest(func):
    def wrapper(*args,**kwargs):
        start_time=time()
        result = func(*args,**kwargs)
        end_time=time()
        print("Elapsed Time: {}".format(end_time - start_time))
        return result
    return wrapper

@timetest
def pow(a,b):
    return a**b
print(pow(500,5))

@timetest
def avg(n):
    if n == 0:
        return 0
    else:
        sum=0
        for i in range(n+1):
             sum=sum+i
        return sum/n
print(avg(599999)) 

Output:

Elapsed Time: 0.0
31250000000000
Elapsed Time: 0.0957489013671875
300000.0

In this example the two function we defined are:

  • pow() to find the power of a number
  • avg() to find the average of n numbers

These two functions are decorated with a generic function called timetest which determines the execution time. Here when you observe you can see that both functions pass different numbers of arguments to the decorator. Even then the decorator runs flawlessly, Can you guess the reason for that ?

This is because in our program we have used the *args and **kwargs in the inner function wrapper  which enables the decorators ability to accept any arbitrary number of positional arguments and keyword arguments.

Functools and wrapper

Ultimately what a decorator does is just wrapping or replacing our function with another function . You can witness  this by trying to print the name of the function as follows for the above program.

print(pow.__name__)
print(avg.__name__) 

The output will be:

wrapper
wrapper

So we ended up losing the information of the functions that were being passed . One way to solve this is by resetting them within the inner function which is considered not as an elegant method.

Luckily python has an alternative way, i.e, we can exploit the functools module which contains functools.wraps. Wraps is a decorator that decorates the inner function by taking the passed function and  copies the attributes like name , docstring, signature etc to the attributes of the inner function.

Examine the below program which shows how we decorated the above example without losing information.

from functools import wraps
from time import time
def timetest(func):
    @wraps(func)    
    def wrapper(*args,**kwargs):
        start_time=time()
        result = func(*args,**kwargs)
        end_time=time()
        print("Elapsed Time: {}".format(end_time - start_time))
        return result
    return wrapper

@timetest
def pow(a,b):
    return a**b
print(pow(500,5))

@timetest
def avg(n):
    if n == 0:
        return 0
    else:
        sum=0
        for i in range(n+1):
             sum=sum+i
        return sum/n
print(avg(599999))

print(pow.__name__)
print(avg.__name__) 

Output:

Elapsed Time: 0.0
31250000000000
Elapsed Time: 0.09993958473205566
300000.0
pow
avg