Decorator Functions in Python.
Tags: python, programming, functional, computerscience
Published: 2021-04-17 18:14:24

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/68vqu04f5q9i2exj9ak3.png)
A decorator function allows you to add or modify an existing function without actually modifying it's structure.

This is a very basic explanation of a decorator function.

But the question here is why should we use a decorator function when we can just change a function as required?

Well, it's not always you have one single function that you might want to modify. Suppose you have a bunch of functions in your project where you want to make a specific change to all the function as a part of your requirement.

Now it would be very tedious to find and modify each and every function and also test each one of them to make sure it does not break your application.

For that you have decorator function using which you can modify it without actually altering any of the code inside the function. Decorator function can have many use cases but is typically used when you want to make minor changes to your existing set of functions.

Let's have a look at the simple example:
```python
def deco_func(val):
    def wrapper():
        print("Trigger")
        print(val)
        print("Kill")
        print("------------------------------")
    return wrapper

holder = deco_func("Hello Python")
print(holder)
holder()
```
Output:
```
<function deco_func.<locals>.wrapper at 0x7efcdc4e8550>
Trigger
Hello Python
Kill
------------------------------
```
The above example is possible in python because the functions here are treated as first class objects, which means that functions can be passed as or used as parameters/arguments.

Here's some quick pointers to keeps in mind:
* A function is an instance of the Object type.
* A function can be stored in a variable.
*  A function can be passed as a parameter.
* A function can return a function.

Now, let's make some minor changes in the above code:
```python
def deco_func(func):
    def wrapper():
        print("Trigger")
        func()
        print("Kill")
        print("------------------------------")
    return wrapper

def func1():
    print("This is Function 1")
    
def func2():
    print("This is Function 2")
    
def func3():
    print("This is Function 3")

func1 = deco_func(func1)
func2 = deco_func(func2)
func3 = deco_func(func3)
print(func1)
func1()
func2()
func3()
```
Output:
```
<function deco_func.<locals>.wrapper at 0x7f6960526820>
Trigger
This is Function 1
Kill
------------------------------
Trigger
This is Function 2
Kill
------------------------------
Trigger
This is Function 3
Kill
------------------------------
```
In the above code we have updated the _deco_func_ and now it accepts a function as an argument.
We have also created three functions that just print a statement.
Now, the line 
_func1 = deco_func(func1)_ 
allows us to store the _deco_func_ which accepts a parameter _func1_, all that in a variable _func1_. Hence, we can now call _func1()_ to get the desired results.
By seeing the above piece of code, you can now figure out a bit, how decorator function works behind the scenes.

So, whenever you are creating a decorator function you have to create this wrapper function/functionality. 
The outer function takes the function itself as an argument and the inner wrapper function calls the actual function upon which you are making the modifications. 

Below is an example of the above code as a decorator function syntax in Python:
```python
def deco_func(func):
    def wrapper():
        print("Trigger")
        func()
        print("Kill")
        print("------------------------------")
    return wrapper
@deco_func
def func1():
    print("This is Function 1")
@deco_func    
def func2():
    print("This is Function 2")
@deco_func    
def func3():
    print("This is Function 3")

func1()
func2()
func3()
```

Now, these are sort of dumb functions that just print a statement.
What happens when one of the functions requires one or more parameters to be passed and some doesn't. Or, some functions return a value and some doesn't??

Below is an example when one or more functions requires and arguments/parameters to be passed or one or more functions happen to return some value back when they are called. The whole point of decorator functions is to be able to use in any functions use-case, hence for that we use the unpack or splat operator i.e. _* args_ and _** kwargs_:
```python
def deco_func(func):
    def wrapper(*args, **kwargs):
        print("Trigger")
        res = func(*args, **kwargs)
        print("Kill")
        print("------------------------------")
        return res
    return wrapper
@deco_func
def func1(val):
    print("This is Function 1")
    print("Function 1 value: ", val)
@deco_func    
def func2(val1, val2):
    print("This is Function 2")
    return val1 + val2
@deco_func    
def func3():
    print("This is Function 3")

func1(20)
result2 = func2(45, 40)
func3()
print("Function 2 sum value: ", result2)
```
Output:
```
Trigger
This is Function 1
Function 1 value:  20
Kill
------------------------------
Trigger
This is Function 2
Kill
------------------------------
Trigger
This is Function 3
Kill
------------------------------
Function 2 sum value:  85
```

As you can see that the splat operator helps the decorator function to accept none or any number of parameters and the _res_ variable stores the returned value of the decorated function to serve it's purpose.

So, That's all about decorator function. I know it's a bit too much to take at a single reading, but I suggest open your python console or jupyter notebook and follow each line of the code snippets to understand the functionality and it will be a piece of cake for you!