Python provides us with many constructs for performing different tasks. While programming, sometimes we may need to modify the working of a function. But we may not be allowed to change the source code of the function as it might be in use in its original form in the program. In such cases, Python decorators can be used.
In this article, we will study what Python decorators are, how we can create decorators, and how we can use them to modify the functionalities of other functions in python.
What is a Python decorator?
Python decorators are functions or other callable objects that can be used to add functionalities to another function without modifying its source code. A decorator in python accepts a function as an input argument, adds some functionalities to it and returns a new function with the modified functionalities.
Implementation of decorators in python requires knowledge of different concepts such as first class objects and nested functions. First, we will take a look at these concepts so that we do not face problems in understanding the implementation of python decorators.
Concepts required to understand decorators
First class objects
In Python, first class objects are those objects that
- can be passed to a function as a parameter.
- can be returned from a function.
- can be assigned to a variable.
All the variables that we use in our programs are first class objects, whether it be a primitive data type, a collection object or objects defined using classes.
Here I want to emphasize that functions in python are also first class objects and we can pass a function as an input parameter or we can return a function from a function.
For Example, let us look at the following source code.
Here, we have defined a function add() that takes two numbers as input and prints their sum. We have defined another function random_adder() that takes a function as input, randomly generates two numbers and calls the input function add() with the randomly generated numbers as input.
import random
def add(num1, num2):
value = num1 + num2
print("In the add() function. The sum of {} and {} is {}.".format(num1, num2, value))
def random_adder(func):
val1 = random.randint(0, 10)
val2 = random.randint(0, 100)
print("In the random_adder. Values generated are {} and {}".format(val1, val2))
func(val1, val2)
# execute
random_adder(add)
Output:
In the random_adder. Values generated are 1 and 14
In the add() function. The sum of 1 and 14 is 15.
From the code and output, you can observe that the function add() has been passed to the random_adder() function as input and the random_adder() function calls the add() function that prints the output.
We can also return a function from another function or callable object. For instance, we can modify the above source code and define a function operate() inside the random_adder() function. The operate() function performs the entire operation done by the random_adder() function in the previous source code.
Now, we can return the operate() function from the random_adder() function and assign it to a variable named do_something. In this way, we will be able to execute the operate() function outside the random_adder() function by calling the variable do_something as follows.
import random
def add(num1, num2):
value = num1 + num2
print("In the add() function. The sum of {} and {} is {}.".format(num1, num2, value))
def random_adder(func):
print("In the random_adder.")
def operate():
val1 = random.randint(0, 10)
val2 = random.randint(0, 100)
print("In the operate() function. Values generated are {} and {}".format(val1, val2))
func(val1, val2)
print("Returning the operate() function.")
return operate
# execute
do_something = random_adder(add)
do_something()
Output:
In the random_adder.
Returning the operate() function.
In the operate() function. Values generated are 3 and 25
In the add() function. The sum of 3 and 25 is 28.
Nested Functions
Nested functions are the functions defined inside another function. For example, look at the following source code.
Here, we have defined a function add() that takes two numbers as input and calculates their sum. Also, we have defined the function square() inside add() that prints the square of the “value” calculated in the add() function.
def add(num1, num2):
value = num1 + num2
print("In the add() function. The sum of {} and {} is {}.".format(num1, num2, value))
def square():
print("I am in square(). THe square of {} is {}.".format(value, value ** 2))
print("calling square() function inside add().")
square()
# execute
add(10, 20)
Output:
In the add() function. The sum of 10 and 20 is 30.
calling square() function inside add().
I am in square(). THe square of 30 is 900.
Free Variables
We know that a variable can be accessed in the scope in which it has been defined. But, In the case of nested functions, we can access the elements of the enclosing function when we are in the inner function.
In the above example, you can see that we have defined the variable “value” inside the add() function but we have accessed it in the square() function. These types of variables are called free variables.
But why the name free variables?
Because it can be accessed even if the function in which it has been defined has completed its execution. For example, look at the source code given below.
def add(num1, num2):
value = num1 + num2
print("In the add() function. The sum of {} and {} is {}.".format(num1, num2, value))
def square():
print("I am in square(). THe square of {} is {}.".format(value, value ** 2))
print("returning square() function.")
return square
# execute
do_something = add(10, 20)
print("In the outer scope. Calling do_something.")
do_something()
Output:
In the add() function. The sum of 10 and 20 is 30.
returning square() function.
In the outer scope. Calling do_something.
I am in square(). THe square of 30 is 900.
Here, Once the add() function returns the square() function, it completes its execution and is cleared from the memory. Still, we can access the variable “value” by calling the square() function that has been assigned to the variable do_something.
Now that we have discussed the concepts needed for implementing python decorators, Let’s dive deep and look how we can implement decorators.
How to create Python Decorators?
We can create python decorators using any callable object that can accept a callable object as input argument and can return a callable object. Here, we will create decorators using functions in Python.
For a function to be a decorator, it should follow the following properties.
- It must accept a function as an input.
- It must contain a nested function.
- It must return a function.
First, we will define a function add() that takes two numbers as input and prints their sum.
def add(num1, num2):
value = num1 + num2
print("The sum of {} and {} is {}.".format(num1, num2, value))
# execute
add(10, 20)
Output:
The sum of 10 and 20 is 30.
Now, we have to define a decorator in such a way that the add() function should also print the product of the numbers along with the sum. For this, we can create a decorator function.
Let us first define a function that takes the add() function as input and decorates it with the additional requirements.
def decorator_function(func):
def inner_function(*args):
product = args[0] * args[1]
print("Product of {} and {} is {} ".format(args[0], args[1], product))
func(args[0], args[1])
return inner_function
Inside the decorator_function(), we have defined the inner_function() that prints the product of the numbers that are given as input and then calls the add() function. The decorator_function() returns the inner_function().
Now that we have defined the decorator_function() and the add() function, let us see how we can decorate the add() function using the decorator_function().
Create Python Decorators By passing functions as arguments to another function
The first way to decorate the add() function is by passing it as an input argument to the decorator_function(). Once the decorator_function() is called, it will return inner_function() that will be assigned to the variable do_something. After that, the variable do_something will become callable and will execute the code inside the inner_function() when called. Thus, we can call do_something to print the product and the sum of the input numbers.
def add(num1, num2):
value = num1 + num2
print("The sum of {} and {} is {}.".format(num1, num2, value))
def decorator_function(func):
def inner_function(*args):
product = args[0] * args[1]
print("Product of {} and {} is {} ".format(args[0], args[1], product))
func(args[0], args[1])
return inner_function
# execute
do_something = decorator_function(add)
do_something(10, 20)
Output:
Product of 10 and 20 is 200
The sum of 10 and 20 is 30.
Create Python Decorators Using @ sign
A simpler way to perform the same operation is by using the “@” sign. We can specify the name of the decorator_function after the @ sign before the definition of the add() function. After this, whenever the add() function is called, It will always print both the product and sum of the input numbers.
def decorator_function(func):
def inner_function(*args):
product = args[0] * args[1]
print("Product of {} and {} is {} ".format(args[0], args[1], product))
return func(args[0], args[1])
return inner_function
@decorator_function
def add(num1, num2):
value = num1 + num2
print("The sum of {} and {} is {}.".format(num1, num2, value))
# execute
add(10, 20)
Output:
Product of 10 and 20 is 200
The sum of 10 and 20 is 30.
In this method, there is a drawback that you cannot use the add() function to just add the numbers. It will always print the product of the numbers along with their sum. So, choose the methodology by which you are going to implement the decorators by properly analyzing your needs.
Conclusion
In this article, we have discussed what python decorators are and how we can implement them using functions in Python. To learn more about python programming, you can read this article on list comprehension. You may also like this article on the linked list in Python.
Recommended Python Training
Course: Python 3 For Beginners
Over 15 hours of video content with guided instruction for beginners. Learn how to create real world applications and master the basics.