Decorators in Python
What are Decorators?
A decorator is a design pattern in Python that allows a user to add new functionality to an existing object (function) without modifying its structure or definition.
This is also called metaprogramming because a part of the program tries to modify another part of the program at compile time.
Prerequisites
- Everything in Python is an OBJECT
Python, under the hood, is implemented in such a manner that everything, whether it is a number, string, list, or even a function gets converted to an object.
We can check this by using the type() function, which returns the class type of the argument(object) passed as a parameter.
In the output, we can see that the type is class. This means that whatever was passed as an argument to the type function was an object.
2. Functions can be passed as arguments to another function
Since functions in Python are also treated as objects, we can pass these functions as an argument to another function.
Such functions that take other functions as arguments are also called higher-order functions. In our case function operation is a higher-order function.
3. We can define a function inside another function and return the nested function
Getting started with Decorators
A decorator function is a function that takes another function as an argument, adds some extra functionality to it and then returns that function.
Explanation:
In the example above, we have a decorator function named timer() and two functions namely function_1() and function_2() which are decorated functions.
Here, the timer() function is the decorator function. This function adds the functionality of measuring the time required for the execution of the decorated function (in this case, function_1() and function_2()).
The timer() function takes the name of the function to be decorated as an argument and the wrapper() function is used to add additional functionality to the function to be decorated.
The wrapper(*args,**kwargs) function has these 2 special arguments, *args and **kwargs. These arguments allow the wrapper() function to accept any length of arguments.
In our case, the function_1() does not take any argument, however, function_2() accepts an argument called sleep_duration. Now instead of defining two separate decorators for these functions, we can define a single wrapper function and have *args,**kwargs as its arguments.
The *args argument is a tuple of arguments required for the decorated function. The **kwargs argument is the dictionary of keyword arguments and their corresponding values.
The syntax of function calls function_1() and function_2(5) internally is processed as follows:
What we see in code:
function_1()
function_2(5)
What internally happens during compilation:
new_version_of_function_1 = timer(function_1)
new_version_of_function_1()
new_version_of_function_2 = timer(function_2)
new_version_of_function_2(5)
The variable new_version_of_function_1 & new_version_of_function_2 are generated by the python interpreter which store the address of wrapper(*args,**kwargs) function and then make the call to the wrapper function. Hence adding the new functionality of measuring the time required for the function execution.
Common Use Cases of Decorators:
- Clocking time required for the execution of a function. Eg: Time required to train an ML model
- Synchronization, i.e. acquiring & releasing locks on the table
- Parameter Type Checking
The source code for this article can be found here.