23.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")
is equivalent to
def ordinary():
    print("I am ordinary")
ordinary = make_pretty(ordinary)

Decorating Functions with Parameters

Example

def divide(a, b):
    return a/b
This function has two parameters, a and b. We know it will give an 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.

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)
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 output would be

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