In python, we can pass functions as arguments to other functions, assign them to variables and return a function from any other function. Also a function can be defined within another function. We can use these properties of functions to add functionalities to the functions without having to change their source code. For this task, we use decorators.

Python Decorators

As the name suggests, decorators are constructs which decorate i.e. add extra functionalities to other functions. In other words, A decorator is a construct that takes a function as an input, adds some kind of functionality to it and returns another function.

We can implement decorators either by using functions or by using classes.In this tutorial, we will look at both ways to implement decorators.
Suppose we have a function ‘add’ which takes two numbers as input and prints their sum as given below.

def add(num1,num2):
    print ("Sum is :" + str(num1+num2))

if __name__ == '__main__':
    add(1,2)

Output:

Sum is :15

Now we want to add a functionality to the function ‘add’ that should also print the difference between the numbers but we don’t want to change the source code of the ‘add’ function. For this task we can use decorators.

1. Python Decorator Functions

There are two ways to implement decorators in python using functions.

1.1 @sign

We can define a decorator function which takes a function as an argument. Inside the decorator function, We define a wrapper function to implement the functionalities that are needed and then return the wrapper function. Also we will have to write @decorator_function_name before the definition of the function which is being passed as a parameter(whose functionalities have to be changed). Wrapper function takes the same arguments as that of the initial function being passed to the decorator function.

Example:

We define a decorator function sum_and_diff and inside that we define a wrapper function wrapper which prints the difference between num1 and num2 and then returns the input function ‘add’ which was given as a parameter after calling it.

#define decorator
def sum_and_diff(func):
    def wrapper(num1,num2):
        print("difference is :" + str(num1-num2))
        return func(num1,num2)
    return wrapper

#define the function
@sum_and_diff
def add(num1,num2):
    print ("Sum is :" + str(num1+num2))
    
#Execute
add(5,10)

Output:

difference is :-5 
Sum is :15

1.2 Assigning to a new variable

In this method , We define the decorator in the same way discussed above but instead of using @decorator_name before the initial function, we assign the wrapper function returned by the decorator to a new variable and then use it as a function.

This method has a benefit that we can use the initial function in its original form even after creating the decorator.

For example, here we have defined the decorator and initial function and then during execution, we use the variable add_diff to assign the function returned by the decorator sum_and_diff to it. Then we use the function ‘add_diff’ instead of ‘add’ as it is having improved functionalities. In this method the ‘add’ function behaves the same way as it was behaving before implementation of the decorator.

#define decorator
def sum_and_diff(func):
    def wrapper(num1,num2):
        print("difference is :" + str(num1-num2))
        return func(num1,num2)
    return wrapper

#define the function
def add(num1,num2):
    print ("Sum is :" + str(num1+num2))
    
#Execute
add_diff=sum_and_diff(add)
add_diff(5,10)
print("_______________")
add(5,10)

Output:

difference is :-5 
Sum is :15
--------------
Sum is :15
If we return ‘func’ instead of ‘func(num1,num2)’ in wrapper functions in decorators of 1.1 and 1.2 then only the new functionalities will be executed and old functionalities won’t. i.e. Difference of numbers will be printed but the sum of numbers will not be printed. So we can also use decorators to entirely change the behaviour of functions instead of just modifying it.

Here we just have to assign the function to a variable, i.e we will have to use dump_var= add(5,10) instead of add(5,10) in 1.1 and dump_var=add_diff(5,10) instead of add_diff(5,10) in 1.2. It is advised that this thing should not be used and new functions should be used for entirely different functionalities.

2. Python Decorator Classes

We know that when we work with objects in python, object_name() is shorthand for object_name.__call__(). We can use classes as decorators if we pass the initial function to the object as parameter and modify the __call__() method to implement the new functionalities. An instance of the decorator class works as a function in this case.

There are two ways to implement decorators using classes.

2.1 @sign

We can define a class which accepts initial function as a parameter and modify the __call__() method to implement the new functionalities. Then we will have to write @decorator_name before the initial function’s definition.

For our example, In the code given below, first we implement a class sum_and_diff_by_class which will accept the ‘add’ function as a parameter whenever it’s instance is created. We modify the __call__() method so that it will print the difference of two numbers and then return ‘add’ method after calling it.  Here we can see that during execution, add() method performs both addition and subtraction operations and prints the result.

#define decorator class
class sum_and_diff_by_class(object):
    def __init__(self,func):
        self.func=func
    def __call__(self,num1,num2):
        print("difference is :"+ str(num1-num2))
        return self.func(num1,num2)

if __name__ == '__main__':
    #define initial function
    @sum_and_diff_by_class
    def add(num1,num2):
        print ("Sum is :" + str(num1+num2))
    #Execute
    add(5,10)

Output:

difference is :-5 
Sum is :15

2.2 Assigning to a new variable

We can create an instance of the class type that of the decorator class and then use the object as a new function.​This method has a benefit that we can use the initial function in it’s previous form.

For example, In the code given below, we don’t use @decorator_class_name before ‘add’ function and during execution, we make an instance ‘new_fun’ of class ‘sum_and_diff_by_class’ and them execute new_fun() as a simple function which will work with added functionalities to ‘add()’ function. Also ‘add()’ function can be used in it’s previous form.

#define decorator class
class sum_and_diff_by_class(object):
    def __init__(self,func):
        self.func=func
    def __call__(self,num1,num2):
        print("difference is :"+ str(num1-num2))
        return self.func(num1,num2)
    
if __name__ == '__main__':
    #define initial function
    def add(num1,num2):
        print ("Sum is :" + str(num1+num2))

    #Execution
    new_fun=sum_and_diff_by_class(add)
    new_fun(5,10)
    print("_____________")
    add(5,10)

Output:

difference is :-5 
Sum is :15 
_____________ 
Sum is :15
If we return self.func instead of self.func(num1,num2) in __call__ methods in decorators of 2.1 and 2.2 then only the new functionalities will be executed and older functionalities won’t. i.e. Difference of numbers will be printed but the sum of numbers will not be printed. So we can also use decorators to entirely change the behaviour instead of just modifying it.

Here We just have to assign the function to a variable, i.e we will have to use ‘dump_var= add(5,10)’ instead of ‘add(5,10)’ in 2.1 and ‘dump_var=new_fun(5,10)’ instead of ‘new_fun(5,10)’ in 2.2.

It is advised that this method should not be used and new functions should be used for entirely different functionalities.

3. References:

Happy Learning 🙂