Python Logo

Objective

In this short blog, I would like to introduce Python decorators to everyone. Some of you might have heard and used it daily. And to those who have never heard of it, let go through some demonstration to show how powerful it can be.

What a Python Decorator is

TL;DR: It is a Python object that is used to modify function or class regarding its behaviour.


Examples

1. Without Decorators

Let’s start with vanilla functions to do some basic arithmetic operations.

def addition(a: int, b: int) -> int:
    return a+b


def substraction(a: int, b: int) -> int:
    return a-b


def multiplication(a: int, b: int) -> int:
    return a*b

Clearly, the results from using those functions are pretty simple and straight forward.

addition(1, 2) # 3
substraction(1, 2) # -1
multiplication(1, 2) # 2

So far so good. What’s the problem with those functions? What if we parse other data types, besides integer, to one of the functions. What will happen? Of course, it will display an error since we have not handled type checking at all.

addition('a', 2)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "main.py", line 8, in addition
    return a+b
TypeError: can only concatenate str (not "int") to str

Let’s fix it by adding type checking to each function. There are a good number of approaches to check data type. For the sake of simplicity, we will use this following condition.

isinstance(num, int) # one parameter

all([isinstance(num, int) for num in [1.....n]]) # multiple parameters

Apply the condition to all existing functions

def addition(a: int, b: int) -> int:
    if all([isinstance(num, int) for num in [a, b]]):
        return a+b


def substraction(a: int, b: int) -> int:
    if all([isinstance(num, int) for num in [a, b]]):
        return a-b


def multiplication(a: int, b: int) -> int:
    if all([isinstance(num, int) for num in [a, b]]):
        return a*b

As you can see, the functions will now only return data when the arguments parsed are all integer

addition(1, -5) # -4
addition(1, 'sd')

The problem of using this approach is that we need to apply the condition to every function that requires type checking which can be tedious.


2. With Decorators

We can simplify all of that by using python decorators. First, we define an outer function to receive a decorated function. Then we create a wrapper function (inner) to deal with all logics we need.

from functools import wraps

def type_check(func):
    wraps(func)
    def wrapper(*args, **kwargs):
        res = None
        if all([isinstance(num, int) for num in [*args]]):
            print('Running function {}'.format(func.__name__))
            res = func(*args, **kwargs)
        return res
    return wrapper

Apply the decorators to all existing functions with @decoration_function_name. As a result, we have a clear and clean code which works as intended.

@type_check
def addition(a: int, b: int) -> int:
    return a+b


@type_check
def substraction(a: int, b: int) -> int:
    return a-b


@type_check
def multiplication(a: int, b: int) -> int:
    return a*b
multiplication(1, 434)
# Running function multiplication
# 434

multiplication(1, []) # None

Wrap Up

A Python decorator is a higher-order function that allows a user to add new functionality to an existing object without modifying its structure. The applications of a decorator can be..

  • Authorization (JWT, etc)
  • Indicate running functions
  • Measure time executed
  • etc.