# Python Decorator, Step by step introduction

### What is Decorator in Python and what problem decorators will try to solve.

#### Let me explain with an example. 
#### Here we have 2 functions , calculate square and calculate cube.

``` 
def calc_square(numbers):
    result = []
    for number in numbers:
        result.append(number*number)
    return result


def calc_cube(numbers):
    result = []
    for number in numbers:
        result.append(number*number*number)
    return result

array = range(1,100000)
out_square = calc_square(array)
out_cube = calc_cube(array)
```
Here, **calc_square** function takes array of numbers as an input and iterating through the array ,calculating the square of a number and putting that into a result. 

**calc_cube** does same thing, but instead of square ,it does cube. 
Here we are calling those function for a range of 1 to 100000.

Now often you have a need of measuring the performance of a function. Performance, I mean how much time does every function take to execute. 
In order to measure the timing, you have to use the ***time*** module.
I will take the **start time** .  Once you are done ,you will take **end time**. 
The code snippet is shown below.

```
import time

def calc_square(numbers):
    start = time.time()
    result = []
    for number in numbers:
        result.append(number*number)
    end = time.time()
    print(" calc_square took " + str((end-start)*1000) + " mil sec ")
    return result
``` 
We do the same thing for **cube** function as well, because we want to measure the performance of both of these functions .


```
def calc_cube(numbers):
    start = time.time()
    result = []
    for number in numbers:
        result.append(number*number*number)
    end = time.time()
    print(" calc_cube took " + str((end-start)*1000) + " mil sec ")
    return result
``` 
The output of the above code will be 

```
calc_square took 27.922630310058594 mil sec 
calc_cube took 41.88823699951172 mil sec 
``` 
Now the problem with this code is that , lets say you have a complex software project and you have written more than **100** functions. In order to measure the performance of all those functions , you have to write the **start time** and **end time**  , exactly the same line of code in every function .

#### Problem No.1 is The start time and end time is getting repeated in every function that you want to measure the performance.
#### Problem no.2 is , there is a  logic in the function(calculating square and cube) and it is combined with the timing logic .It makes code less readable. 

![Untitled.jpg](https://cdn.hashnode.com/res/hashnode/image/upload/v1619446356662/XYuXFgf0i.jpeg)

Now there has to be a better way of doing this and better way is basically a **Decorator**.

**Decorator** allows you to wrap your function in another function .  
Lets remove **timing logic** or **timing code** from above functions. And I want to have a function which has just the logic that function is supposed to do.  

In order to do decorator , you need to define the wrapper function and lets call it as **time_it(func)** and that wrapper function will take function **(func)** as an argument.

Now **functions** are **1st class objects** in Python means:


•	You can pass functions as an argument to function. 


•	Also, you can return function as a return value from another function. 

So in **time_it(func)** : we will define another function called wrapper (Python allows us to write nested function i. e we can write one function inside another function) and what wrapper function is doing is it is taking the positional arguments ***args** and keyword arguments which is ****kwargs**

And then it will start the timer and then it will call the function that was passed as an argument (So I am going to call function here) with argument and keyword argument 

Then I will measure the end time. 


```
The print(func.__name__ + “ took ” + str((end-start)**1000 + “ mil sec ”)
``` 
then return result

Then return wrapper function.  i.e return wrapper

```
 __name__
```  
will return the name of that function. 
Here we are returning a **function (wrapper)** from another function **time_it** , that’s why this function **wrapper** is called ** 1st class object** . 
You can treat it as normal variable, you can return it and pass it as a function argument and so on. 

```
def time_it(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args,**kwargs)
        end = time.time()
        print(func.__name__ + " took " + str((end-start)*1000) + " mil sec ")
        return result
    return wrapper
``` 

Also we need to decorate the **calc_square** and **calc_cube** function
i.e  @time_it which is shown below 


```
@time_it
def calc_square(numbers):
    result = []
    for number in numbers:
        result.append(number*number)
    return result

@time_it
def calc_cube(numbers):
    result = []
    for number in numbers:
        result.append(number*number*number)
    return result

```

So, any function that you want to measure the performance of now, once you have defined this **time_it** function , you can put the ** @time_it ** tag the beginning and it is going to measure the performance . It makes both the function **calc_square** and **calc_cube** more readable and all your timing code is restricted into one function.

#### Final thoughts :

Decorator acts as a wrapper to the original function.







