Python Decorators - Plain @powerful

3 Min. Read
May 28, 2021

@decorators in python

If you have used @something syntax in python, you have used a decorator before. A decorator is any callable python object that is used to modify a function, method or class definition - Wikipedia. What it means is that, you can actually extend functionality of a python object or method without actually rewritting it, 😱😱 amazing, right?

So for a simple example, lets create a scenario, say we have a function that takes two numbers and returns their sum.

1
2
def add(a, b):
    return a + b

Now, you need to find out how long does it take to that function to run. So, you might do something like,

1
2
3
4
5
6
7
8
9
import logging


def add(a, b):
    t1 = time.perf_counter()
    result = a + b
    t2 = time.perf_counter()
    logging.info(f'add() took {t2-t1} second/s.')
    return result

Yeah, that works, but now lets say you already have another function, that takes a list of integers and return their product, and another function that sends email, another function that prints next world cup winner and whole lot of other functions. And for each of them you need to log time taken to execute that function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def multipy(*ar):
    t1 = time.perf_counter()
    rv = math.prod(*ar)
    t2 = time.perf_counter()
    logging.info("...")
    return rv


def send_email(receivers, *ar, **kw):
    t1 = time.perf_counter()
    with smtplib.SMTP(*ar) as server:
        ...
    t2 = time.perf_counter()
    logging.info("...")

So, you can see how repetitive and time consuming it can be and how ugly and messy your code can look. Since we are adding exactly the same functionality to each of existing functions, this would be the perfect scenario to use the decorator.

Basically, decorators are the function that takes another function as an argument, add addtional functionality and returns the original function without altering it (the original function).

Now to achieve it, decorator wraps the original function inside a wrapper function.

1
2
3
4
5
def decorator_function(original_function):
    def wrapper_function():
        # some feature
        return original_function()
    return wrapper_function

In our case, out decorator would look something like,

1
2
3
4
5
6
7
def log_duration(original_function):
    def wrapper_function(*ar, **kw):
        t1 = time.perf_counter()
        rv = original_function(*ar, **kw)
        t2 = time.perf_counter()
        logging.info(f"{original_function.__name__} took {t2-t1} seconds to complete.")
    return wrapper_function

Now our decorator is ready to use. You can re-assign your existing function to be new decorated function with that existing function passed as an argument.

1
2
3
add = log_duration(add)
multiply = log_duration(multiply)
send_email = log_duration(send_email)

Or you can use @decorator syntax over your existing function.

1
2
3
4
5
6
7
8
9
10
11
12
@log_duration
def add(a, b):
    return a + b

@log_duration
def multiply(*ar):
    return math.prod(*ar)

multiply(1,2,3,4,5)
>> 120
# decorators.log
multiply took 0.021 seconds to complete.

We can create decorators using class as well. And as you may have seen somewhere before, we can create decorators that takes in arguments. What we have above is just a very simple example, just to get you started, so that you can explore more for yourself 👍!

References

Official Wiki

Wikipedia

PEP0318