In Python, a decorator takes a function and adds some functionality and returns it.
Everything in python is an objects. In Python, everything is an object—yes, including classes. Simply said, names defined by us are identifiers attached to these objects. There are no exceptions, functions are also objects (with attributes). The same function object can have multiple names linked to it.
1
2
3
4
5
6
7
8
9
10
11
def first(msg):
print(msg)
first('Hello')
second = first
second('Hello')
Output:
'Hello'
'Hello'
Both the first and second methods produce the same result when the code is executed. Here, the same function object is referred to by both the first and second names.
Functions can be passed as arguments to another function. Higher order functions are another name for such functions that accept other functions as arguments. Some of the examples of such functions are:- map
, filter
, and reduce
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def inc(x):
return x + 1
def dec(x):
return x - 1
def operate(func, x):
result = func(x)
return result
print(operate(inc,3))
Output: 4
print(operate(dec,3))
Output: 2
Furthermore, a function can return another function.
1
2
3
4
5
6
7
8
9
def is_callled():
def is_returned():
print('Hello')
return is_returned
new = is_called()
new()
Output: 'Hello'
Here, is_returned()
is a nested function which is defined and returned each time we call is_called()
.
Getting back to Decorators
Functions and methods are called callable as they can be called. In actuality, the term “callable” refers to any object that implements the special call() method. So a decorator is a callable that returns another callable in the most basic sense. A decorator basically accepts a function, adds some functionality, and then returns it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def make_pretty(func):
def inner():
print('I got decorated.')
func()
return inner
def ordinary():
print('I am ordinary.')
ordinary()
Output: 'I am ordinary.'
#let's decorate this orindary function
pretty = make_pretty(ordinary)
pretty()
Output:
I got decorated.
I am ordinary.
In the example shown above, make_pretty()
is a decorator.
1
pretty = make_pretty(ordinary)
The function ordinary() got decorated and the returned function was given the name pretty.
We can see that the decorator function expanded the original function’s capabilities. This is comparable to gift-wrapping. As a wrapper, the decorator serves. The actual gift inside the adorned object retains its original essence. But now, it appears lovely (since it got decorated). In most cases, we rename a function and decorate it as,
1
ordinary = make_pretty(ordinary)
This is a common construct and for this reason, Python has a syntax to simplify this.
We can use the @ symbol along with the name of the decorator function and place it above the definition of the function to be decorated. For example:
1
2
3
@make_pretty
def ordinary():
print('I am ordinary.')
is equivalent to
1
2
3
def ordinary():
print('I am ordinary.')
oridnary = make_pretty(ordinary)
This is just a syntactic sugar to implement decorators.
Decorating Functions With Parameters
The decorator mentioned above was straightforward and merely utilized functions without any parameters. What if we had functions that accepted the following parameters:
1
2
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
.
1
2
3
4
5
6
7
8
divide(2,5)
Output: 0.4
divide(2,0)
Output:
Traceback (most recent call last):
...
ZeroDivisionError: division by zero
Now writing a decorator to check for this case that causes error like:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def smart_divide(func):
def inner(a,b):
print(f'I am going to divide {a} by {b}.')
if b==0:
print(f'Whoopsies! cannot divide by zero.')
return
return func(a,b)
return inner
@smart_divide
def divide(a,b):
return a/b
divide(2,5)
Output:
'I am going to divide 2 by 5.'
0.4
divide(2,0)
Output:
'I am going to divide 2 and 5.'
'Whoopsies! cannot divide by zero.'
This new implementation will return None if the error condition arises.
We can decorate functions that take parameters in this way. A careful observer will notice that the parameters of the decorator’s nested inner() function match those of the functions it decorates. With this in mind, we can now create universal decorators that function with any quantity 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:
1
2
3
4
5
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
Python allows for the chaining of several decorators. This means that a function may be decorated more than once by the same or distinct decorators. The decorators are simply positioned above the desired function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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)
@start
@percent
def printer(msg):
print(msg)
printer('Hello')
Output:
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Hello
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************
The above sytanx of:
1
2
3
4
@star
@percent
def printer(msg):
print(msg)
is equivalent to
1
2
3
def printer(msg):
print(msg)
printer = star(percent(printer))
The order in which we chain decorators matter. If we had reversed the order as,
1
2
3
4
@percent
@star
def printer(msg):
print(msg)
The output would be:
1
2
3
4
5
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************
Hello
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%