Friday, March 20, 2020

Python Decorators

Decorators

Decorator functions are design patterns used to extend or modify the functionality of the existing code. Decorators are used to dynamically modify the functionality of a function, method, or class. There is no need to directly use subclasses or alter the original code of the decorated function. This is also known as Metaprogramming when one part of code changes the behavior of the other part of the code. Decorators can prove to be a powerful tool in the software development process. 

Decorator function

A decorator takes a function as input, puts some additional functionality and returns it. A decorator enhances the behavior of other functions or methods. Any function taking other function as a parameter and returning an enhanced or extended function can be utilized as a decorator. 

Everything in the Python, Even the classes, are objects. Functions are no exceptions, they are also the objects having no attributes. Multiple different identifiers can be bound to the same function object. 

For example

def a_function():
   print("Hello world")
b_function=a_function
b_function()

This is same as,

def c_function(f):
   return f
@c_function
def d_function():
  print("Hello World")

c_function(d_function())


The @-notation is syntactic sugar. This is the simplest decorator 


d_function =c_function(d_function)


This type of minimal decorator can be occasionally used as a code marker

The decorator function must take the decorated function as an argument and return the decorated function. If decorator function returns nothing than the decorated function is removed from the local scope. For example

def disabler(f):
  #returns Nothing, it means decorated function    # is out of the scope  pass
@disabler
def fun():
  print('This will not be printed')
fun()
#ERROR

Output:
TypeError: 'NoneType' object is not callable

Writing decorator

We can define a new function inside the decorator function and return this function. This new function can something additional that it requires to do, then eventually it will invoke the original function and finally return the value. Let's take this simple decorator function that displays the arguments of the original function and then calls it.

def test_args(fun):
  def fun_inner(*args,**kwargs):
    print(args)
    print(kwargs)
    return fun(*args,**kwargs)

    #invoke the original function with arguments.    return fun_inner

@test_args
def mul(a,b):
  return a * bprint(mul(4, 7))

Output:
(4, 7)
{}
28

OR

def test_div(func):
   def inner_func(x,y):
      print("Dividing",x,"and",y)
      if y == 0:
         print("Sorry! Wrong input cannot divide")
         return      return func(x,y)
   return inner_func

@test_div
def divide(x,y):
    return x/y
print(divide(5,0))
print(divide(5,4))

Output:
Dividing 5 and 0
Sorry! Wrong input cannot divide
None
Dividing 5 and 4
1.25

Decorator class


Basically, a decorator is a function that is applied to another function to extend its functionality. The syntactic sugar is equivalent to the following: 


fun = decorator(fun) 


Functions and methods are called callable as they can be called. Any object in Python h implements the method __call__() is known as callable. It means a decorator is a callable object. Additionally, a decorator takes in a function, adds some functionality and returns it. 

But can a Python class be used as a decorator? 

The answer is yes, A class can be defined as a decorator, for this, we just need to use a special method __call__ to make the class callable.

class Decorator:
    def __init__(self, fun):
        self.fun = fun
    def __call__(self):
        print('The code before the function call')
        self.fun()
        print('The code after the function call')

#adding class as decorator
@Decorator
def function():
    print("ProgrammingHunk Python Tutorials")

function()

Output:
The code before the function call
ProgrammingHunk Python Tutorials
The code after the function call

Decorator Chaining in Python


We can make a chain of decorators in Python. We can decorate a function multiple times with single or multiple decorators, 

For Example

def pattern1(fun):
    def inner_fun(*args, **kwargs):
        print("=" * 21)
        fun(*args, **kwargs)
        print("=" * 21)

    return inner_fun

def pattern2(fun):
    def inner_fun(*args, **kwargs):
        print("#" * 21)
        fun(*args, **kwargs)
        print("#" * 21)

    return inner_fun

@pattern1
@pattern2
def display(msg):
    print(msg)

display("###ProgrammingHunk###")

Output:
=====================
#####################
###ProgrammingHunk###
#####################
=====================


The output will be changed according to the decorator sequence in chaining,

def pattern1(fun):
    def inner_fun(*args, **kwargs):
        print("=" * 21)
        fun(*args, **kwargs)
        print("=" * 21)

    return inner_fun

def pattern2(fun):
    def inner_fun(*args, **kwargs):
        print("#" * 21)
        fun(*args, **kwargs)
        print("#" * 21)

    return inner_fun

@pattern2
@pattern1
def display(msg):
    print(msg)

display("###ProgrammingHunk###")

Output:
#####################
=====================
###ProgrammingHunk###
=====================
#####################