![](https://static.wixstatic.com/media/9d30f2_1531686989d5493ba1b493a4c26616c9~mv2.jpg/v1/fill/w_344,h_146,al_c,q_80,enc_avif,quality_auto/9d30f2_1531686989d5493ba1b493a4c26616c9~mv2.jpg)
Photo credit to Maia
If you're a Python developer, you've likely come across decorators in your code or heard about them in discussions about Python's advanced features. In essence, decorators are a powerful and versatile tool in Python that allow you to modify the behavior of functions and methods at runtime. They offer a clean and elegant way to wrap, enhance, or modify the functionality of your code without changing the original code itself. In this blog post, we'll explore what decorators are, how they work, and how you can use them to improve your Python code. So, let's dive into the world of Python decorators!
The fundamental idea behind ...
Most of us must have come across the word first class objects in python. Fancier as it might sound it just means you can pass them to other functions as arguments, returned as values, and be stored in variables and data structures. Decorators simply uses this property of python to modify the behaviour of the original function by wrapping it within another function. A simple yet elegant syntax to represent decorators is the @ symbol followed by the name of the decorator function just above the function you want to decorate.
Think of it as a cherry on top of a cake used as a decoration ;)
If you're wondering about the scenarios where decorators can be used, the common scenarios include ...
Adding a logger or timer to a function without changing the function code.
implement authentication or authorization checks before function execution.
memoization which is a technique of caching the function result to improve performance.
Getting our hands dirty
Lets say you are given a class which has a main() method that calls many other functions to perform a specific task. But for some reason this main() method is taking alot of time to execute and you need to find which of the function inside main() is responsible for this. You are not allowed to change the code inside any function. What should you do ?
Decorators to the rescue !
As discussed above since functions are first class object they can be passed to other function. So lets wrap this function in another function called timer.
def timer(my_func):
def wrapper():
start = time()
my_func()
print('Executed in {} secs'.format(time() - start))
return wrapper
The purpose of the timer function which acts as a decorator is to return a wrapper function which simply performs the decorative task on the function and returns the result of the original function. The decorative task here being the display of execution time of the function.
How to use decorators
But how do we use this decorator function in our code. Well python being super intuitive and super easy to understand provides a syntactic sugar abstracting away all the complexities being handled under the hood. Lets say i have a function named list_students() which as its name suggests gives out a list of students based on some logic. The below code shows how to find the execution time for the execution of the function.
@timer
def list_students():
students = ['Rohit', 'Virat', 'Pujara']
return students
Behind the curtains
For most of us the syntactic sugars work fine. But there are few whose pet ants dont let them sleep without knowing where this sugar came from. So lets deep dive into the internals of how using a decorator function with an "@" just above the original function works. In the above example what python does behind the scene looks something like this
list_students = timer(list_students)
Python functions being first class objects can be passed to other functions and can be assigned to variables. You must be realizing here how important the concept of first class objects is for decorators. So as you can see here we have passed our list_students() function as argument to timer() func which does nothing but return its own internal function whose task is to execute that function and perform the decorative task. This is the basic step for any decorator. The internal function is now saved to the variable whose name is same as that of your original function. Coincidence ?
Well actually python very smartly overrides the original function with the internal function returned from the decorator. So now when you call list_students() you actually are calling that internal function. So no coincidence but sheer elegance.
Decorate function with params
In the above code we looked at a very simple case of decorating a function which had no parameters. But if the function has some params we dont see any place where the params could be passed with the original function. In such case where we have no idea about the params of a function we can access them with *args, **kwargs.
Lets see how we can use this amazing feature of python to decorate parameterized params.
# The function which will be decorated
def list_students(num_students):
students = ['Rohit', 'Virat', 'Pujara']
return students[:num_students]
# decorator function
def timer(func):
def wrapper(*args, **kwargs):
start = time()
func(*args, **kwargs)
print('Executed in {} secs'.format(time() - start))
return wrapper
In this example we modified our original function to just return the asked number of students.
But if we see the internal implementaion of decorators (explained above) we are not passing any parameters, just the wrapper() function returned from timer is assigned to list_students variable. So now whenever you call list_students as a function with some parameters then these parameters are automatically passed to wrapper() function and can be accessed through *args and **kwargs. So inside our wrapper() function when the original function is called then those params can be passed to it. Play a little with the above code by printing the args and kwargs values to get a better understanding.
Comments