'Big O' in Python with decorator
Tags: python, decorator, bigo
Published: 2019-10-03 15:46:49

---
title: 'Big O' in Python with decorator
published: true
tags: python, decorator, bigO
canonical_url: https://tech.serhatteker.com/post/2019-06/python-dunder-call/
---

We sometimes —always?, want to know how efficient our code is. So here comes the magical [Big O](https://guide.freecodecamp.org/computer-science/notation/big-o-notation) 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:


```python
#!/usr/bin/env python
# -*- coding: utf-8 -*-

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.


```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

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](https://www.python.org/dev/peps/pep-0318/)

Also I highly encourage to use python built-in [`logging`](https://docs.python.org/3/library/logging.html) method instead of
`print`.

```python
import logging

logger = logging.getLogger(__name__)


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