Decorators can be thought of as functions which modify the functionality of another function. This is also called metaprogramming as one part of the program tries to modify another part of program at compile time. It can also be used to add functionality to the existing code.
There are 2 types of decorators:
To understand decorators better lets start with some important aspects of functions. First you have to know or remember that function names are references to functions and that we can assign multiple names to the same function:
In [6]:
def func(n):
return n + 1
x = func(1)
print(x)
y = func(2)
print(y)
Here x and y refers to the same function object.
The next important fact is that we can delete either "succ" or "successor" without deleting the function itself.
In [7]:
del y
print(x)
In [8]:
print(y)
In [17]:
def f():
def g():
print("Hi, it's me 'g'")
print("Thanks for calling me")
print("This is the function 'f'")
print("I am calling 'g' now:")
g()
f()
In [10]:
def temperature(t):
def celsiusToFarhenheit(x):
return 9 * x / 5 + 32
result = "It's " + str(celsiusToFarhenheit(t)) + " degrees"
return result
temperature(10)
Out[10]:
In [29]:
def inc(x):
return x + 1
def dec(x):
return x - 1
def operate(func,x):
result = func(x)
return result
operate(inc,1)
Out[29]:
In [30]:
operate(dec,2)
Out[30]:
In [18]:
def f(x):
def g(y):
return y + x + 3
return g
nf1 = f(1)
nf2 = f(3)
print(nf1(1))
print(nf2(1))
In [20]:
def make_pretty(func):
def inner():
print("I got decorated")
func()
return inner
def ordinary():
print("I am ordinary")
ordinary()
In [21]:
pretty = make_pretty(ordinary)
pretty()
In the example shown above the function make_pretty() is a decorator. The 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. This is similar to packing a gift. The decorator acts as a wrapper. The nature of the object that got decorated does not alter. But now, it looks pretty.
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 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,
@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.
In [23]:
@make_pretty
def ordinary():
print("I am ordinary")
ordinary()
The above decorator was simple and it only worked with functions that did not have any parameters. What if we had functions that took in parameters like below?
def divide(a,b):
return a/b
The function has 2 parameters a and b. We know, it will give error if we pass in b as 0.
In [25]:
def divide(a,b):
return a/b
divide(2,5)
divide(2,0)
To avoid this error lets make a decorator to check for this case.
In [26]:
def smart_divide(func):
def inner(a,b):
if b == 0:
print('Cannot divide by zero')
return
return func(a,b)
return inner
@smart_divide
def divide(a,b):
return a / b
In [27]:
divide(5,2)
Out[27]:
In [28]:
divide(5,0)
If you observered closely the number of parameters of the nested inner() function inside the decorator is same as the parameters of functions it decorates. Taking this into account, now we can make general decorators that work with any number of parameter.
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 decorator will be.
In [31]:
def works_for_all(func):
def inner(*args, **kwargs):
print('I can decorate any function...')
return func(*args, **kwargs)
return inner
In [35]:
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")
This is equivalent to:
In [37]:
def printer(msg):
print(msg)
printer = star(percent(printer))
printer('Hello')
Please note the order in which we chain the decorators also matter.
In [36]:
@percent
@star
def printer(msg):
print(msg)
printer('Hello')
In [ ]: