Getting Started with Decorators in Python
Tags: progamming, python, beginners, decorators
Published: 2021-11-10 16:55:18

To get started with Decorators , First we have to understand few things :-

1. everything we see in python is an object.
2. function in python is also an object.
3. we can declare function within a function.
4. these inner function have access to local variables in th e enclosing function. 
5. a function can return any value, including a function

to understand the above point let us take an example

###Example :-
```
>>> def hello(name):
...     return f"Hello {name}"
...
>>> a = hello
>>> # as we can see variable a referencing to a function object
>>> a("Ram")
'Hello Ram'
>>>
```
with the above example we can,
* functions are objects 
* they can be assigned/reference by other name ,such as variable *a*

let's take another example to clear other remaining 
points
```
>>> def foo(name):
...     def bar():
...             print(f"Hey I'm the inner function bar enclosed inside foo function , {name}")
...     return bar
...
>>> f = foo("Ram")
>>> f.__name__
'bar'
>>> type(f)
<class 'function'>
>>> f()
Hey I'm the inner function bar enclosed inside foo function , Ram
>>>
```

In the above example we have declared a function *foo* which contains another function *bar*, and when we called the outer function *foo* we get back reference to *bar* function object,
we also verified the same by,
* checking *f.__name__* attribute which return name of the function
* checked *type(f)* to see that it is of type *function*

* Inner function *bar* has access to  all varibles of enclosing function like variable *name*


The concepts that we have covered in the above example
will help in the understanding of Decorators

Now we see some examples of special type of decorator

### __Function Wrapper__
Wrappers around the functions are also knows as decorators which are a very powerful and useful tool in Python since it allows programmers to modify the behavior of function or class.

Let's understand with an example

### Example :-
```
lines = '-' * 60 + '\n'

def with_lines(func):
    def wrapper():
        return f'{lines}{func()}{lines}'
    return wrapper

def a():
    return f'I am in a!\n'
new_a = with_lines(a)


def b():
    return f'I am in b!\n'
new_b = with_lines(b)

print(a())
print(b())
print(new_a())
print(new_b())

Output :-
I am in a!

I am in b!

------------------------------------------------------------
I am in a!
------------------------------------------------------------

------------------------------------------------------------
I am in b!
------------------------------------------------------------


``` 
In the above example we created a *with_lines* function which takes *func* (callable object ) as and argument and *with_lines* function has an inner function *wrapper*
which has access to the outer function variable, 
So we call the function passed by outer function inside the __wrapper__ function by adding *global varible* lines at the start and end and return wrapper function object.

With this now we have a *with_lines* function wrapper which can we used with any function to change behiviour of the function,

As we used it with  *new_a* and *new_b* functions that have some formating added to them using *with_lines* wrapper function.

Wrapper are one of the most powerful tools of python which can we used to change or added some extra behiviour to the existing function.


Now we have gathered enough information to understand *Decorators*.

### What is a Decorator?
In simple terms :-
* A function
* that takes function as an argument 
* and return a function as its output
* Because of the **@** syntax in Python, decorating a function replaces that function with the result of calling the decorator on the function.

### What can I use a Decorator For?

* Anything that is repeated across multiple functions
* Grab / modify / filter input arguments
* Grab / modify / filter outputs
* Change the state
* Stop a function from being called under certain conditions
* Choose a different function to be called instead

Now we will understand decorators with couple of examples

### Example :-
```
lines = '-' * 60 + '\n'

def with_lines(func):
    def wrapper(*args, **kwargs):
        return f'{lines}{func(*args, **kwargs)}{lines}'
    return wrapper

@with_lines
def a():
    return f'I am in a!\n'
# a = with_lines(a)   # this line (after) is the same as @with_lines (before)


@with_lines
def b():
    return f'I am in b!\n'
# b = with_lines(b)  # this line (after) is the same as line 14

@with_lines
def add(x, y):
    return f'{x} + {y} = {x+y}\n'

print(a())
print(b())
print(add(3,5))  # add is actually wrapper! so when I call add(3,5), it's saying wrapper(3,5)

OUTPUT :- 

------------------------------------------------------------
I am in a!
------------------------------------------------------------

------------------------------------------------------------
I am in b!
------------------------------------------------------------

------------------------------------------------------------
3 + 5 = 8
------------------------------------------------------------

```
Using the same *with_lines* wrapper function used in the above example we added this decorator with the existing method *a()* ,*b()* and *add()* method using using **@** Symbol which replaces that function with the result of calling the decorator on the function.

Let's take another example to see how we can filter the input using decorator :-
We have given a list of int from which we have to all numbers which are only even, let's see how we can do it using *decorator*

### Example :-
```


# Wrapper that filter even from list of ints
def only_evens(func):
	def wrapper(*args):
		even_numbers = [num for num in args if num % 2 == 0]
		print("\n\nWrapper added")
		print(f"even numbers {even_numbers}")
		result = f"sum of even numbers :- {func(*even_numbers)}"
		return result
		
	return wrapper



def add_nums(*numbers):
	sum = 0
	for num in numbers:
		sum += num
	return sum


print("Original add_nums function output :-",add_nums(1,2,3,4,5,6,7,8,9,10))

@only_evens
def add_nums(*numbers):
	sum = 0
	for num in numbers:
		sum += num
	return sum


print("Original add_nums function output :-",add_nums(1,2,3,4,5,6,7,8,9,10))

Output :-
Original add_nums function output :- 55


Wrapper added
even numbers [2, 4, 6, 8, 10]
Original add_nums function output :- sum of even numbers :- 30

```

In the above example 
first we called the orignal method *add_nums()* which add all the numbers.
then we declared the *add_nums()* function again but this time 
added **@only_evens** wrapper to it , the  wrapper *only_evens* filter the list of values first then the filters values passed to the original method *add_nums()* which return the sum of only even values.

with this example we have come to an end,
hope you all have learn some basis of *decorators*
this topic , at first is not easy to grasp,
but as you explore more about *decorators*
you will know how useful it is.
See you next time :-)