Pages

Thursday, May 20, 2021

Decorators in Python

Decorators

In simple words, Decorator is a function that calls another function. 

Decorators are very useful concept in Python. Decorators allow the programmers to enhance the functionality of a function by wrapping it with a decorator. 

It is similar to having a function with in a function. Before we see more about decorators let's have a quick look at how it is like to wrap a function within another function and how a function can be assigned like an object. These will help in understanding the decorators better. 

Assigning function as an Object

Like creating instance of a class (object), the same can be done with functions as well. 

We can define a function and create instances of a function. Function can either be called directly or by using the instance created. 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

# Define a function to print "Hello World"

def hello_world():

   print("Hello World, This is a original function")



# Call the function directly

hello_world()


# Create instance of the function

say_hello = hello_world


# Calling the instance of a function

print("--Calling the instance of a function--")

say_hello()



Result

Hello World, This is a original function

--Calling the instance of a function--

Hello World, This is a original function


In the above example, 
  • Line - 2: Defining 'hello_world' function without any arguments. 
  • Line - 7: Calling the function 'hello_world'.
  • Line - 10: Assigning the function 'hello_world' with a different name 'say_hello'. 
    • One thing to note here is while assigning a function to create an instance, We should not specify round brackets after the function name. 
    • Mentioning the round brackets would call the function and assigns the return value to the variable. 
  • Line - 13: Print statement before calling the instance of a function. 
  • Line - 14: Calling the function 'say_hello' which also executes the functionality provided with in 'hello_world'.

One thing to note here is, assigning the function would create a new function and would be able to execute even after the deletion of the original function. 

16

17

18

19

20

# Delete the function 'hello_world'

del hello_world


say_hello()



Wrap a function with another function

Let's have a look at creating a function to wrap another function. This is usually required when the wrapper function need to contain some additional functionality. 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

# Define a function to print "Hello World"

def hello_world():

   print("Hello World, This is a original function")



# Wrapper function

def say_hello():

   print("--Inside wrapper function--")

   hello_world()

   print("--End of wrapper function--")



# Call Wrapper function to call hello_world

say_hello()



Result


--Inside wrapper function--

Hello World, This is a original function

--End of wrapper function--


In the above example, 
  • Line - 2: Defining the 'hello_world' function which just prints a message. 
  • Line - 7: Defining another function 'say_hello' with some additional messages. 
    • Lines - 8 & 10: To understand better, We are only printing Start and End of a wrapper function. 
  • Line - 9: Calling 'hello_world' function, which prints "Hello World" message. 
  • Line - 14: Calling 'say_hello' function to call 'hello_world' with added functionality from wrapper function.
This is easy and straight forward. However, if we need to create a wrapper for 10 functions or even more, we would need to create 10 different functions. 

Passing Function as parameter to wrapper function

One solution for this is passing function as a parameter. Let's have a look at an example by having two different functions and one wrapper function. 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

# Define a function to print "Hello World"

def hello_world():

   print("Hello World, This is a original function")



def hello():

   print("Hello, This is another function")



# Wrapper function

def say_hello(new_function):

   print("--Inside wrapper function--")

   new_function()

   print("--End of wrapper function--")



# Call Wrapper function by passing function as parameter

say_hello(hello_world)


# Call Wrapper function by passing function as parameter

say_hello(hello)



Result


--Inside wrapper function--

Hello World, This is a original function

--End of wrapper function--

--Inside wrapper function--

Hello, This is another function

--End of wrapper function--


In the above example, 
  • Line - 2 & 6: Two different functions defined with different message to be printed. 
  • Line - 11: Defining a wrapper function 'say_hello' by passing a function as an argument. 
    • 'new_function' is an argument and function can be called by using this name. 
  • Line - 13: Calling the function by using the name mentioned in the arguments. 
    • Function called would be the function passed. 
  • Line - 18: Call function 'say_hello' and pass function 'hello_world' as an argument. 
    • One thing to note here, round brackets should not be used after the function in the arguments. 
  • Line - 21: Call function 'say_hello' and pass function 'hello' as an argument. 
    • One thing to note here, round brackets should not be used after the function in the arguments. 
However, Where ever the original function is called, the code need to be modified to call wrapper function instead. 

This can be avoided by using decorators. 

Creating a decorator

Using decorators makes this process much easier. 
  • Decorators can be used with functions that require arguments.
  • Calling programs doesn't require to be modified to call the new wrapper function. 
  • Same decorator can be with different functions. 
Let's have a look at an example of a decorator for a function with no arguments. 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

# Create a Decorator

def say_hello_decorator(new_function):


   # Wrapper function inside a decorator

   def wrapper():

       print("--Inside wrapper function--")

       new_function()

       print("--End of wrapper function--")


   return wrapper



# Define a function to print "Hello World"

# Add a decorator before the function definition

@say_hello_decorator

def hello_world():

   print("Hello World, This is a original function")



# When decorator is mentioned with the function

# function can be called on it's down

# Wrapper in the decorator would be executed automatically

hello_world()



Result


--Inside wrapper function--

Hello World, This is a original function

--End of wrapper function--


In the above example, 
  • Line - 1: Defining a decorator with a function as an argument. 
    • Decorator need to be mentioned against the function by using '@'. 
    • This function would be referred with the name in the arguments in the decorator. 
  • Line - 5: Defining a wrapper function with arguments needed for the original function to be called. 
    • No arguments passed in this case as the original function 'hello_world' doesn't require any arguments to be passed. 
  • Lines - 6 & 8: Print statements to print the message for beginning and end of decorator wrapper function. Can be any code required to be performed as part of wrapper function. 
  • Line - 7: Function call, with the function name passed in the arguments. 
  • Line - 10: Return of wrapper, name of the function (with out round brackets). This is required to execute the wrapper function defined inside decorator function. 
  • Line - 16: Defining the function 'hello_world'. There is no change in the way function is coded. If decorator needs to be used, this needs to be added before the function definition. 
  • Line - 15: Mentioning decorator against the function, name of the decorator function by prefixing with '@'. 
    • This makes the whole process easier compared to writing the wrapper to enclose the function directly. 
    • No change needs to be done in the way original function is called (Line - 23). 
    • If this line is removed, the original function call will only execute the function defined and not decorator. 
  • Line - 23: No change to be done in the calling program. This is a huge advantage when the function is being used in large number of programs. 

Creating a decorator for a function with arguments

In the previous example, we have seen how to create a decorator for a function with no arguments. 

So, How to deal with a function with arguments? Wrapper function inside the decorator needs to be created with the required arguments. Arguments passed to the original function would automatically be passed over to the wrapper function in decorator. 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

# Create a Decorator

def say_hello_decorator(new_function):


   # Wrapper function inside a decorator

   def wrapper(name):

       print("--Inside wrapper function--")

       new_function(name)

       print("--End of wrapper function--")


   return wrapper



# Define a function to print "Hello World"

# Add a decorator before the function definition

@say_hello_decorator

def hello_world(name):

   print(f"Hello World, {name} here.")



# When decorator is mentioned with the function

# function can be called on it's down

# Wrapper in the decorator would be executed automatically

hello_world("Pradeep")



Result


--Inside wrapper function--

Hello World, Pradeep here.

--End of wrapper function--


In the above example, we have added an argument to the original function and the wrapper function. 
  • Line - 16: Defining the function 'hello_world' with one argument. No change in the way decorator is mentioned before the function definition. 
  • Line - 5: Defining the wrapper function inside the decorator with the arguments needed. Arguments passed to the original function (Line - 23) will be passed over to the wrapper inside the decorator. 
  • Line - 23: Calling the the function by passing the required arguments. 
There is no other change required to this process. 

Creating a decorator for a function with return value

What if the function needs to return a value back to the calling program? Return value needs to be captured and returned at the end of the wrapper function. 

One thing to note here is, if the return value is mentioned in middle of wrapper function, execution would stop there (like any other function). 

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

# Create a Decorator

def say_hello_decorator(new_function):


   # Wrapper function inside a decorator

   def wrapper(name):

       print("--Inside wrapper function--")

       ret = new_function(name)

       print("--End of wrapper function--")

       return ret


   return wrapper



# Define a function to print "Hello World"

# Add a decorator before the function definition

@say_hello_decorator

def hello_world(name):

   print(f"Hello World, {name} here.")

   return "Successful"



# When decorator is mentioned with the function

# function can be called on it's down

# Wrapper in the decorator would be executed automatically

ret_value = hello_world("Pradeep")


print(ret_value)



Result


--Inside wrapper function--

Hello World, Pradeep here.

--End of wrapper function--

Successful


In the above example, we have only done couple of changes to include return statement in the original function and in the wrapper function. 
  • Line - 19: Adding a return statement inside the original function.
  • Line - 7: Assigning the return value to the variable inside the wrapper function. 
  • Line - 9: Returning the value from the wrapper function. This would return the return value back to the calling function. 
  • Line - 25: Assigning the return value of the function. No change needs to be done for this when a decorator is added to the function. 

Hope the above info was useful in understanding the how decorators work in Python. 


If you have any Suggestions or Feedback, Please leave a comment below or use Contact Form.

Exception Handling in Python (try - except-else-finally)

Exception Handling in Python Exception handling is key for any programming language. When an exception occurs Python interpreter stops execu...