Python Decorators

A decorator accepts a function and adds functionality to it before returning it. You’ll learn how to make a decorator and why you should use it in this article.

Decorators in Python

Decorators are a very powerful and useful tool in Python since it allows programmers to modify the behaviour of function or class. Decorators allow us to wrap another function in order to expand its behavior without having to change it permanently. But, before we get too far into decorators, let us first grasp a few principles that will help us study the decorators.

Prerequisites for learning decorators

In order to understand about decorators, we must first know a few basic things in Python.

We must accept the fact that everything in Python is an object (yes, including classes). The names we provide to these things are merely identifiers. Functions are not exempt; they, too, are objects (with attributes). The same function object can have several names assigned to it.

Here is an example.

def first(msg):
    print(msg)


first("Hello")

second = first
second("Hello")

Output

Hello
Hello

When you run the code, both the first and second functions produce the identical result. The first and second names here correspond 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 mapfilter and reduce in Python, then you already know about this.

Higher order functions are defined as functions that take other functions as arguments. Here’s an example of a function like this.

def inc(x):
    return x + 1


def dec(x):
    return x - 1


def operate(func, x):
    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()

# Outputs "Hello"
new()

Output

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.


Getting back to Decorators

Functions and methods are called callable as they can be called.

Callable refers to any object that implements the special call() method. A decorator is, in the most basic sense, 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")

When you run the following codes in shell,

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

The decorator function, as we can see, provided some new functionality to the original function. It’s similar to packaging a present. The decorator performs the function of a wrapper. The nature of the decorated object (the real present within) remains unchanged. However, it now appears to be attractive (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 place the @ symbol above the definition of the function to be decorated, along with the name of the decorator function. As an 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 decorator above was simple, and it only worked with functions that had no parameters. What if there were functions that took parameters such as:

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.

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)

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.

The parameters of the nested inner() function inside the decorator are the same as the parameters of the functions it decorates, as a keen observer would discover. With this in mind, we can now create universal decorators that can handle any amount of parameters.

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 a 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 means that a function can be decorated with different (or the same) decorators many times. The decorators are simply placed above the required 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)


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
******************************
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

“Thanks For Reading”

Leave a Comment

Your email address will not be published.