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

# 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

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})
``````