Newby Coder header banner

Python Decorators

Python has a feature called decorators to add functionality to an existing code

It is similar to Java Annotations

What are decorators in Python?

A decorator takes in a function, adds some functionality and returns it

This is also called metaprogramming as a part of the program tries to modify another part of the program at compile time

Prerequisites

Decorators are callable

Functions and methods are called callable as they can be called

In python, any object which implements the method __call__() is termed callable

So, in a basic sense, a decorator is a callable that returns a callable

Basically, a decorator takes in a function, adds some functionality and returns it

Consider the following:

def decorate(fn):
    def wrapper():
        print("━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┫")
        fn()
        print("━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┫")
        print("wrapper is executed")
    return wrapper

def print_str():
    print("print_str() function is executed")

print_str()

new_fn = decorate(print_str)
new_fn()
new_fn()

Output

print_str() function is executed
━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┫
print_str() function is executed
━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┫
wrapper is executed
━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┫
print_str() function is executed
━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┫
wrapper is executed

In the example shown above, decorate() is a decorator

In the assignment step new_fn = decorate(print_str), the function print_str() got decorated (i.e. wrapped inside a new function) and the returned function was given the name new_fn

The decorator function decorate() added some new functionality to the original function

To simplify the syntax (in terms of re-usability), the @ symbol can be used along with the name of decorator function and placed above the definition of the function to be decorated

def decorate(fn):
    def wrapper():
        print("━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┫")
        fn()
        print("━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┫")
        print("wrapper is executed")
    return wrapper

@decorate
def print_str():
    print("print_str() function is executed")

print_str()

Output

━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┫
print_str() function is executed
━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┫
wrapper is executed

Decorating Functions with Parameters

Conside print_str() function shown above, is changed to a function which takes 2 arguments and prints them

def print_str(value1, value2):
    print(value1, value2)

The decorator wrapper wrapper() function has to be modified to pass any received parameters to the decorated function

def decorate(fn):
    def wrapper(*args, **kwargs):
        print("━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┫")
        fn(*args, **kwargs)
        print("━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┫")
        print("wrapper is executed")
    return wrapper

@decorate
def print_str(value1, value2):
    print(value1 + " and " + value2)

print_str("A string", "another string input to decorated print_str()")

Output

━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┫
A string and another string input to decorated print_str()
━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┫
wrapper is executed

This way, general decorators can be made that work with any number of parameter

Following decorator can be used to handle exceptions and print exception and prevent program from exiting

def handle_exception(fn):
    def wrapper(*args, **kwargs):
        try:
            return fn(*args, **kwargs)
        except Exception as e:
            print("Exception:", e)
            return None
    return wrapper

@handle_exception
def divide(value1, value2):
    return float(value1) / float(value2)

divide(10, 32)
divide("123", 0)

As shown, it can be used with a divide() function to handle exceptions during division and conversion to float

Chaining Decorators in Python

Multiple decorators can be chained in Python, allowing a function to be decorated multiple times with different (or same) decorators

The decorators are simply placed above the desired function

def decorate(fn):
    def wrapper():
        print("━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┓")
        fn()
        print("━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┛")
    return wrapper

def decorate_star(fn):
    def wrapper():
        print("***************************************************************************************")
        fn()
        print("***************************************************************************************")
    return wrapper

@decorate
@decorate_star
def print_str():
    print("print_str() function is executed")

print_str()

Output

━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┓
***************************************************************************************
print_str() function is executed
***************************************************************************************
━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┛

Here, at first the print_str() function is decorated by decorate_star() function and the wrapper function which is returned by decorate_star() is then passed to decorate() function which wraps it and returns the wrapper

The above syntax of

@decorate
@decorate_star
def print_str():
    print("print_str() function is executed")

is equivalent to

def print_str():
    print("print_str() function is executed")

print_str = decorate(decorate_star(print_str))
The order in which the decorators appear, matter

If the order for above example is reversed as

@decorate_star
@decorate
def print_str():
    print("print_str() function is executed")

then its displayed as :

***************************************************************************************
━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┓
print_str() function is executed
━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━━┅┅┅┅┅━━━━━┛
***************************************************************************************