Decorators
A decorator takes in a function, adds some functionality and returns it.
Functions and methods are called callable as they can be called.
Basically, a decorator is a callable that returns a callable.
Example
def make_pretty(func):
def inner():
print("I got decorated")
func()
return inner
def ordinary():
print("I am ordinary")
>>> ordinary()
I am ordinary
>>> # let's decorate this ordinary function
>>> pretty = make_pretty(ordinary)
>>> pretty()
I got decorated
I am ordinary
In the above example, make_pretty() is a decorator. Th function ordinary() got decorated and the returned function was given the name pretty.
We can see that the decorator function added some new functionality to the original function.
Generally, we decorate a function and reassign it as,
ordinary = make_pretty(ordinary)
This can be simplied in python by using @
symbol along with the name of the decorator function and place it above the definition of the function to be decorated.
With decorator
@make_pretty
def ordinary():
print("I am ordinary")
def ordinary():
print("I am ordinary")
ordinary = make_pretty(ordinary)
Decorating Functions with Parameters
Example
def divide(a, b):
return a/b
>>> 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.
Example
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)
>>> 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
From the above example, we can notice that parameters of the nested inner() function inside the decorator is the same as the parameters of functions it decorates. Therefore, we can make general decorators that work with any number of parameters.
In Python, this magic is done as function(args, *kwargs). As we know, args will be the tuple of positional arguments and kwargs will be the dictionary of keyword arguments.
Example
def works_for_all(func):
def inner(*args, **kwargs):
print("I can decorate any function")
return func(*args, **kwargs)
return inner
Chaining Decorators
Multiple decorators can be chained in Python.
This is to say, a function can be decorated multiple times with different (or same) decorators. We simply place the decorators above the desired function.
Example
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)
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
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%