Showing posts with label Python Decorator. Show all posts
Showing posts with label Python Decorator. Show all posts

Python Decorator



According to Bruce Eckel's Introduction to Python Decorators "Decorators allow you to inject or modify code in functions or classes". In other words decorators allow you to wrap a function or class method call and execute some code before or after the execution of the original code. And also you can nest them e.g. to use more than one decorator for a specific function. Usage examples include - logging the calls to specific method, checking for permission(s), checking and/or modifying the arguments passed to the method etc.

Python Decorator- (what ,types and why)

What?
  • Decoration is a  way to specify management code for functions and classes
  • Decorator themselves take the form of callable objects (ie., function ) that process other callable objects 
Types?
  • Decorator are mainly two type
    • function Decorator
    • class Decorator
  • function Decorator install wrapper object to intercept later function calls and process them as needed
  • class Decorator install wrapper objects to intercept later instance creation calls and process them as required
Why?
  • Decorators have very explicit syntax which makes them easier to spot than helper function class that may be arbitrarily far removed from the subject functions or classes.
  • Decorators are applied once, when the subject function or class is defined; its not necessary to add extra code at every call to the class or function

Python Decorators

Python has an interesting feature called decorators to add functionality to an existing code. This is also called metaprogramming as a part of the program tries to modify another part of the program at compile time.

Preliminaries

In order to understand about decorators, we must first know a few basic things in Python. We must be comfortable with the fact that, everything in Python (Yes! Even classes), are objects. Names that we define are simply identifiers bound to these objects. Functions are no exceptions, they are objects too (with attributes). Various different names can be bound to the same function object. Here is an example.
>>> def first(msg):
...     print(msg)
...     

>>> first("Hello")
Hello

>>> second = first
>>> second("Hello")
Hello
Here, the names first and second refer to the same function object.
Now things start getting weirder. Functions can be passed as arguments to another function. If you have used functions like map, filter and reduce in Python, then you already know about this. Such function that take other functions as arguments are also called higher order functions. Here is an example of such a function.
def inc(x):
    """Function to increase value by 1"""
    return x + 1

def dec(x):
    """Function to decrease value by 1"""
    return x - 1

def operate(func, x):
    """A higer order function to increase or decrease"""
    result = func(x)
    return result
We invoke the function as follows.
>>> operate(inc,3)
4
>>> operate(dec,3)
2
Furthermore, a function can return another function.
>>> def is_called():
...     def is_returned():
...         print("Hello")
...     return is_returned
... 

>>> new = is_called()
>>> new()
Hello
Here, is_returned() is a nested function which is defined and returned, each time we call is_called().
Finally, we must know about closures in Python.

Back To Decorators

Functions and methods are called callable as they can be called. In fact, any object which implements the special method __call__() is termed callable. So, in the most basic sense, a decorator is a callable that returns a callable. Basically, a decorator takes in a function, adds some functionality and returns it.
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 example shown above, make_pretty() is a decorator. In the assignment step.
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 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 (actual gift inside) does not alter. But now, it looks pretty (since it got decorated).
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.

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
This function has two parameters, a and b. We know, it will give error if we pass in b as 0.
>>> 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.
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):
    return a/b
This new implementation will return None if the error condition arises.
>>> 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
In this manner we can decorate functions that take parameters. A keen observer will notice that 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. 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 decorator will be.
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. 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.
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)
This will give the output.
>>> printer("Hello")

******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Hello
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
******************************
The above syntax of,
@star
@percent
def printer(msg):
    print(msg)
is equivalent to
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 execution would take place as,
>>> printer("Hello")

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