Python Decorator
Python Decorator: A Decorator is an interesting feature of Python that helps the user design patterns and insert a new functionality to an existing object without making any modifications in its structure. Usually, we call Decorators before defining the function that we want to decorate. We will discuss a lot about the decorators and their uses in Python Programming. Moreover, this type programming is also known as metaprogramming because, in this programming, one part of the program is modified by another during the compilation time and vice versa.
The functions in Python are First-Class Object, which means they support various operations. For example - Being returned from a function passed as an argument, modified, assigned to a variable and many more. Let's start exploring how to implement decorators in the Python Programs and many in the following sections.
Prerequisites to learn Python decorators
Before we jump into Python decorators' depth, there are some fundamentals we must need to know.
Starting with the fact that everything in Python (Even the classes) are objects. The Names that we define works as identifiers that are bounded to these objects. There are no exceptions that even Functions are also objects (with attributes). We can bound different names to the same function object. The same is illustrated in the following example.
Example -
def initial(message): print(message) initial("Hello There!") final = initial final("Hello There!")
The above snippet of code should produce an Output, as shown below:
Output:
Hello There! Hello There!
As we can observe, we are getting the same result from both the functions, initial and final, respectively. And the name initial and final refers to the same function object.
Now, let’s explore a bit deeper into this section. We can also pass one function to another as an argument. This functionality can be seen in various functions such as filter, map and reduce in Python. Such functions are also known as higher-order functions as these functions take other functions as arguments. The same is demonstrated in the following example:
def increment(a): return a + 2 def decrement(a): return a - 2 def activate(function, a): res = function(a) return res
The above function can be invoked using a python interpreter, as shown below:
activate(increment, 4) 6 activate(decrement, 4) 2 Moreover, a function can also return another function. def iscalled(): def isreturned(): print("Welcome to TutorialandExample.com") return isreturned its_new = iscalled() # Output "Welcome to TutorialandExample.com" its_new() Welcome to TutorialandExample.com
Output:
In the above example, we can observe that a nested function, namely isreturned(), is defined and return each time we call iscalled().
Getting back to Python Decorators
First of all, the methods and functions are Callable because of their ability to be called. Any object can be termed as callable if it implements the special method called __call__(). Thus, we can say that a decorator is also a callable that helps return a callable.
More specifically, a decorator takes a function as input, adds some functionality to it and returns it. We can see the following example for more understanding.
def pretty(function): def inside(): print("This function is decorated!!") function() return inside def the_ordinary(): print("This is an ordinary function.") The above snippet of code should produce the following output once executed in the python interpreter. print(the_ordinary())
Output:
This is an ordinary function.
# Let’s start decorating this ordinary function the_pretty = pretty(the_ordinary) print(the_pretty())
Output:
This function is decorated!!
In the above example, we have to use the function pretty() as a decorator. In the assignment step:
the_pretty = pretty(the_ordinary)
We have decorated the function the_ordinary() function and give the name the_pretty() to the returned function.
As we can observe that the original function is decorated using the decorator function that added some new functionality to it. This is kind of similar to gift packaging. The decorator works as a wrapper. It does not alter the nature of the object that got decorated, i.e., the real gift inside. However, the present looks pretty now (since it got decorated).
Usually, a function is decorated and reassigned as follows:
the_ordinary = pretty(the_ordinary)
The syntax shown above is a common construct and can be easily simplified in Python.
This can be done using the @ symbol alongside the decorator function name and placing it above the function definition that is to be decorated. Let’s see the following example:
@pretty def the_ordinary(): print("This is an ordinary function.")
is equivalent to
def the_ordinary(): print("This is an ordinary function.") the_ordinary = pretty(the_ordinary)
The above example is simply a cube of syntactic sugar for implementing Python decorators.
Decorating Functions with Parameters
The decorator shown above was quite simple and usually worked with functions with no parameters. But how to decorate the functions that take in parameters such as:
def div(x, y): return x / y
The above function has two parameters, x and y. And we know that it will return an error if we pass in y as 0.
print(div(3,2)) 1.5 print(div(4,0)) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 2, in div ZeroDivisionError: division by zero
Now, we will be making a decorator for checking for this case that will cause the error.
def smart_div(function): def inside(x, y): print("Let's start dividing", x, "and", y) if y == 0: print("Ugh! Poor choice of numbers. This cannot divide.") return return function(x, y) return inside @smart_div def divide(x, y): print(x/y)
Let’s try the above snippet of code and see the output we get:
Print(div(3,2)) Let's start dividing 3 and 2 1.5 print(div(4,0)) Let's start dividing 4 and 0 Ugh! Poor choice of numbers. This cannot divide.
As we can observe, the decorator function implementation will return none whenever the error persists. And Thus, we can decorate the functions with parameters.
Chaining Decorators in Python
We can chain multiple decorators in Python. This statement implies that we can decorate a function multiple times with the same or different decorators. All we need is to place the decorator above the chosen function. Here’s an example is shown below:
def inverted_commas(function): def inside(*first, **second): print("'" * 35) function(*first, **second) print("'" * 35) return inside def stars(function): def inside(*first, **second): print("*" * 35) function(*first, **second) print("*" * 35) return inside @inverted_commas @stars def the_printer(message): print(message) the_printer("WELCOME TO TUTORIALANDEXAMPLE.COM")
The above snippet of code should produce an Output, as shown below:
''''''''''''''''''''''''''''''''''' *********************************** WELCOME TO TUTORIALANDEXAMPLE.COM *********************************** '''''''''''''''''''''''''''''''''''
The above syntax of
@inverted_commas @stars def the_printer(message): print(message)
is equivalent to
def the_printer(message): print(message) the_printer = inverted_commas(stars(the_printer))
In addition to the above, one more factor affects the decorators' chaining. This factor is the order of chaining the decorators. If we had reversed the order as
@stars @inverted_commas def the_printer(message): print(message)
The Output would be:
*********************************** ''''''''''''''''''''''''''''''''''' WELCOME TO TUTORIALANDEXAMPLE.COM ''''''''''''''''''''''''''''''''''' ***********************************