Python Decorators



Decorators in Python

A decorator is a function that takes another function and extends the behavior of the giving function without explicitly modifying it.

The primary purpose of decorators is to add functionality to an existing code.

The mechanism of decorators in Python is called metaprogramming because a part of the program tries to change another part of the program at compile time.


Prerequisites for learning decorators

To understand decorators, we need to know some basic things that can be given in the following list.

  • Everything in Python are objects (Even classes).
  • Names that we define are simply identifiers bound to the given objects.
  • Functions are also objects and various names can be bound to the same function object.

Let us consider the following example.

def show_str(str):
    print(str)

show_str("Hello World")

another_func = show_str
another_func("Hello World")

Output

Hello World
Hello World

As we can see above, both functions show_str and another_func return the same output. Here, the names show_str and another_func refer to the same function object.

In Python, functions can be passed as arguments to another function.

You already used functions that take other functions as arguments if you have used functions like filter, map and reduce.

In Python, functions that accept other functions as arguments are called higher order function. Let us see an example of such a function.

def inc(x):
    return x + 1

def dec(x):
    return x -1

def perform(func, x):
    result = func(x)
    return result

Now, we can invoke the function as follows.

>>> perform(inc, 4)
5
>>> perform(dec, 7)
6

A function can also return another function.

def called_func():
    def returned_func():
        print("Hello World")
    return returned_func

func = called_func()

# Ouputs "Hello World"
func()

Output

Hello World

Above, returned_func is a nested function that is defined and returned each time we invoke called_func.

Finally, to understand Python decorators, we also need to know about Closures in Python.


Deep dive into Decorators

Functions and methods are named callable because they can be called.

In Python, any object that implements the ___call__() method is named callable. So, in the basic sense, a decorator is callable that returns a callable.

By definition, a decorator is a function that takes another function and adds some functionality, and returns it.

def decorate(func):
    def inner():
        print("I got decorated")
        func()
    return inner

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

Now, we can run the following code in Python shell.

>>> simple()
I am ordinary

>>> # here, we decorate the simple() function
>>> nice = decorate(simple)
>>> nice()
I got decorated 
I am ordinary

Above, the deocrate() function is a decorator.

In the assignment step:

nice = decorate(simple)

The function simple() got decorated, and the returned function was named nice.

As we can see above, the decorator function added some new functionality to the original function. The decorator serves as a wrapper. The nature of the object that got decorated does not change. But now, it looks nicer.

Usually, we decorate a function and reassign it as,

simple = decorate(simple)

This is a common construct, and this is why Python has a syntax to simplify this.

So to decorate a function, we can just use the @ symbol along with the name of the decorator function and place it above the definition of the function to be decorated.

@decorate
def simple():
    print("I am ordinary")

The code above is equivalent to

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

nice = decorate(simple)

Decorating Functions with Parameters

We already saw how a simple decorator that does not have any parameters works. But what about if we want functions that took in parameters like:

def inverse(x):
    return 1/x

This function has one parameter, x. We know it will raise an error if we pass in x as 0.

>>> inverse(5)
0.2
>>> inverse(0)
Traceback (most recent call last)
...
ZeroDivisionError: division by zero

Now let us define a decorator to check for this case that will cause the error.

def smart_inverse(func):
    def inner(x):
        print("I am going to inverse", x)
        if x == 0:
            print("Cannot inverse")
            return

        return func(x)
    return inner

@smart_inverse
def inverse(x):
    return 1/x

The above implementation will return None if the error condition occurs.

>>> inverse(5)
I am going to inverse 5
0.2    

>>> inverse(0)
I am going to inverse 0
Cannot inverse

This way, we can decorate functions that take parameters.

As we can see above, the parameters of the nested inner() function inside the decorator are the same as the parameters of functions it decorates. So now we can make general decorators that work with any number of parameters.

Decorating function with parameters is done as function(*args, **kwargs). In this case, args will be the tuple of positional arguments and kwargs will be the dictionary of keyword arguments.

def runs_for_all():
    def inner(*args, **kwargs):
        print("I can decorate any functions")
        return func(*args, **kwargs)
    return inner

Chaining Decorators in Python

Python offers the possibility to chain multiple decorators together.

So, a function can be decorated multiple times with different or the same decorators. We just place the decorators above the desired function.

def hyphen(func):
    def inner(*args, **kwargs):
        print("-" * 20)
        func(*args, **kwargs)
        print("-" * 20)
    return inner

def star(func):
    def inner(*args, **kwargs):
        print("*" * 20)
        func(*args, **kwargs)
        print("*" * 20)
    return inner

@star
@hyphen
def show(msg):
    print(msg)

show("Hello World")

Output

********************
--------------------
Hello World
--------------------
********************

The following syntax:

@star
@hyphen
def show(msg):
    print(msg)

The above code is equivalent to

def show(msg):
    print(msg)

show = star(hyphen(show))

The order in which decorators are chained matters. Suppose we had reversed the order.

@hyphen
@star
def show(msg):
    print(msg)

show("Hello World")

The output would be:

--------------------
********************
Hello World
********************
--------------------


ExpectoCode is optimized for learning. Tutorials and examples are constantly reviewed to avoid errors, but we cannot warrant full correctness of all content. While using this site, you agree to have read and accepted our terms of use, cookie and privacy policy.
Copyright 2020-2021 by ExpectoCode. All Rights Reserved.