Decorators

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:

  • Function Decorators
  • Class 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)


2
3

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)


2

In [8]:
print(y)


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-8-36b2093251cd> in <module>()
----> 1 print(y)

NameError: name 'y' is not defined

Functions within functions


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()


This is the function 'f'
I am calling 'g' now:
Hi, it's me 'g'
Thanks for calling me

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]:
"It's 50.0 degrees"

Functions as Parameters

Functions can be passed as arguments to another function. Such functions that take other functions as arguments are called Higher Order Functions.


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]:
2

In [30]:
operate(dec,2)


Out[30]:
1

Functions returning functions

The output of a function is also a reference to an object.


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))


5
7

In [20]:
def make_pretty(func):
    def inner():
        print("I got decorated")
        func()
    return inner

def ordinary():
    print("I am ordinary")

ordinary()


I am ordinary

In [21]:
pretty = make_pretty(ordinary)
pretty()


I got decorated
I am ordinary

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()


I got decorated
I am ordinary

Decorating Functions with Parameters

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)


---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-25-c29fd25c7352> in <module>()
      3 
      4 divide(2,5)
----> 5 divide(2,0)

<ipython-input-25-c29fd25c7352> in divide(a, b)
      1 def divide(a,b):
----> 2     return a/b
      3 
      4 divide(2,5)
      5 divide(2,0)

ZeroDivisionError: division by zero

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]:
2.5

In [28]:
divide(5,0)


Cannot divide by zero

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

Chaining Decorators in Python

Multiple decorators can be chained in python.


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")


******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Hello
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************

This is equivalent to:


In [37]:
def printer(msg):
    print(msg)
    
printer = star(percent(printer))
printer('Hello')


******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Hello
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************

Please note the order in which we chain the decorators also matter.


In [36]:
@percent
@star
def printer(msg):
    print(msg)
    
printer('Hello')


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************
Hello
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

In [ ]: