Table of Contents
A decorator accepts a function and adds functionality to it before returning it. You’ll learn how to make a decorator and why you should use it in this article.
Decorators in Python
Decorators are a very powerful and useful tool in Python since it allows programmers to modify the behaviour of function or class. Decorators allow us to wrap another function in order to expand its behavior without having to change it permanently. But, before we get too far into decorators, let us first grasp a few principles that will help us study the decorators.
Prerequisites for learning decorators
In order to understand about decorators, we must first know a few basic things in Python.
We must accept the fact that everything in Python is an object (yes, including classes). The names we provide to these things are merely identifiers. Functions are not exempt; they, too, are objects (with attributes). The same function object can have several names assigned to it.
Here is an example.
def first(msg):
print(msg)
first("Hello")
second = first
second("Hello")
Output
Hello Hello
When you run the code, both the first and second functions produce the identical result. The first and second names here correspond to the same function object.
Now things start getting weirder.
Functions can be passed as arguments to another function.
If you have used functions like map
, filter
and reduce
in Python, then you already know about this.
Higher order functions are defined as functions that take other functions as arguments. Here’s an example of a function like this.
def inc(x):
return x + 1
def dec(x):
return x - 1
def operate(func, x):
result = func(x)
return result
We invoke the function as follows.
>>> operate(inc,3)
4
>>> operate(dec,3)
2
Furthermore, a function can return another function.
def is_called():
def is_returned():
print("Hello")
return is_returned
new = is_called()
# Outputs "Hello"
new()
Output
Hello
Here, is_returned()
is a nested function which is defined and returned each time we call is_called()
.
Finally, we must know about Closures in Python.
Getting back to Decorators
Functions and methods are called callable as they can be called.
Callable refers to any object that implements the special call() method. A decorator is, in the most basic sense, a callable that returns a callable.
Basically, a decorator takes in a function, adds some functionality and returns it.
def make_pretty(func):
def inner():
print("I got decorated")
func()
return inner
def ordinary():
print("I am ordinary")
When you run the following codes in shell,
>>> ordinary()
I am ordinary
>>> # let's decorate this ordinary function
>>> pretty = make_pretty(ordinary)
>>> pretty()
I got decorated
I am ordinary
In the example shown above, make_pretty()
is a decorator. In the assignment step:
pretty = make_pretty(ordinary)
The function ordinary()
got decorated and the returned function was given the name pretty
.
The decorator function, as we can see, provided some new functionality to the original function. It’s similar to packaging a present. The decorator performs the function of a wrapper. The nature of the decorated object (the real present within) remains unchanged. However, it now appears to be attractive (since it got decorated).
Generally, we decorate a function and reassign it as,
ordinary = make_pretty(ordinary).
This is a common construct and for this reason, Python has a syntax to simplify this.
We can place the @ symbol above the definition of the function to be decorated, along with the name of the decorator function. As an example,
@make_pretty
def ordinary():
print("I am ordinary")
is equivalent to
def ordinary():
print("I am ordinary")
ordinary = make_pretty(ordinary)
This is just a syntactic sugar to implement decorators.
Decorating Functions with Parameters
The decorator above was simple, and it only worked with functions that had no parameters. What if there were functions that took parameters such as:
def divide(a, b):
return a/b
This function has two parameters, a and b. We know it will give an error if we pass in b as 0.
>>> divide(2,5)
0.4
>>> divide(2,0)
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
Now let’s make a decorator to check for this case that will cause the error.
def smart_divide(func):
def inner(a, b):
print("I am going to divide", a, "and", b)
if b == 0:
print("Whoops! cannot divide")
return
return func(a, b)
return inner
@smart_divide
def divide(a, b):
print(a/b)
This new implementation will return None
if the error condition arises.
>>> divide(2,5)
I am going to divide 2 and 5
0.4
>>> divide(2,0)
I am going to divide 2 and 0
Whoops! cannot divide
In this manner, we can decorate functions that take parameters.
The parameters of the nested inner() function inside the decorator are the same as the parameters of the functions it decorates, as a keen observer would discover. With this in mind, we can now create universal decorators that can handle any amount of parameters.
In Python, this magic is done as
function(*args, **kwargs)
. In this way, args
will be the tuple of positional arguments and kwargs
will be the dictionary of keyword arguments. An example of such a decorator will be:
def works_for_all(func):
def inner(*args, **kwargs):
print("I can decorate any function")
return func(*args, **kwargs)
return inner
Chaining Decorators in Python
Multiple decorators can be chained in Python.
This means that a function can be decorated with different (or the same) decorators many times. The decorators are simply placed above the required function.
def star(func):
def inner(*args, **kwargs):
print("*" * 30)
func(*args, **kwargs)
print("*" * 30)
return inner
def percent(func):
def inner(*args, **kwargs):
print("%" * 30)
func(*args, **kwargs)
print("%" * 30)
return inner
@star
@percent
def printer(msg):
print(msg)
printer("Hello")
Output
****************************** %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% Hello %%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ******************************
The above syntax of,
@star
@percent
def printer(msg):
print(msg)
is equivalent to
def printer(msg):
print(msg)
printer = star(percent(printer))
The order in which we chain decorators matter. If we had reversed the order as,
@percent
@star
def printer(msg):
print(msg)
The output would be:
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ****************************** Hello ****************************** %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
“Thanks For Reading”