We sometimes —always?, want to know how efficient our code is. So here comes the magical Big O notation:

Big O notation is used in computer science to describe the performance or complexity of an algorithm. Actually Big O notation is special symbol that tells you how fast an algorithm is. Of course you’ll use predefined algorithms often — and when you do, it’s vital to understand how fast or slow they are.

Efficiency covers lots of resources, including:

  • CPU (time) usage
  • Memory usage
  • Disk usage
  • Network usage

All are important but we will talk about CPU time: Time complexity or CPU usage. We can evaluate it as below:

#!/usr/bin/env python3


import datetime


def foo():
    print("foo bar")


start_time = datetime.datetime.now()
foo()
finish_time = datetime.datetime.now()
duration = finish_time - start_time

print(f"Execution time: {duration}")

Is there any better method for it? Yes, with a decorator:

What are decorators in Python?

Python has an interesting feature called decorators to add functionality to an existing code.

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

Decorators allow us to wrap another function in order to extend the behavior of wrapped function, without permanently modifying it.

In decorators, functions are taken as the argument into another function and then called inside the wrapper function.

#!/usr/bin/env python3


import time
import random
import functools


def time_decorator(func):
    """ Calculate the execution time of a method and return it back"""
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        duration = time.time() - start

        print(f"Duration of {func.__name__} function was {duration}.")
        return result
    return wrapper


# with a function
@time_decorator
def sum_random_numbers(ran_num):
    """ Return the sum between 0 and ran_num"""
    total_sum = 0
    for num in range(0, ran_num):
        total_sum += num
    return total_sum


# with a class instance method
class Klass:
    @time_decorator
    def sum_random_numbers(self, ran_num):
        """ Return the sum between 0 and ran_num"""
        total_sum = 0
        for num in range(0, ran_num):
            total_sum += num
        return total_sum


if __name__ == "__main__":
    # generate a random number between 1.000.000 and 2.000.000
    ran_num = random.randint(1_000_000, 2_000_000)
    print(f"random_number: {ran_num}")

    sum_random_numbers(ran_num=ran_num)
    Klass().sum_random_numbers(ran_num=ran_num)

If we run the code the output will be like:

random_number: 1123388
Duration of sum_random_numbers function was 0.05038046836853027.
Duration of sum_random_numbers function was 0.04922914505004883.

So we can use this decorator to evaluate our efficiency everywhere in our codes with ease.


Appendix

For more detail about decorators in Python: Decorators for Functions and Methods

Also I highly encourage to use python built-in logging method instead of print.

import logging

logger = logging.getLogger(__name__)


logger.info(f"Execution time: {self.func.__name__} function was {duration})