Decorators in Python.
Tags: python, programming, tutorial, webdev
Published: 2023-06-05 00:08:35

Decorators are a powerful tool in Python for adding custom functionality to classes and functions. They are defined to classes and functions. They are defined a similar way to functions, but with the addition of a decorator keyword. In this article, we will be learning `when?` to use a decorator and `why` we need decorators with some few examples.

## What are decorators?
A decorator is a function that is attached to another function or class. It takes one or more arguments, and its job is to modify the behavior of the original function or class.

Before we begin on how to create a decorator, we need to understand the fundamentals of functions before we dive right in. For more understanding about functions, you can visit this article [Understanding functions in Python](https://link.medium.com/3fjxoChvXzb).
So let's create our functions.

```
def func1():
    print("Called func1")
func1()
```
Results:

```
Called func1
```
Now rather than print func1 or calling func1, we will print func1 without he brackets and see what happens.

```
def func1():
    print("Called func1")
print(func1)
```
Output:

```
<function func1 at 0x000002CB9EEA3E20>
```
You can see some random memory address, what this actually means is that: ` func1` is an object and since it is an object, we can pass it all round our program. To understand this better, let's create another function called `func2`.

```
def func1():
    print("Called func1")
def func2(f):
    f()
func2(func1)
```
the function `func2` has been created and it takes, and argument called `f` which is called below the function. Now since func1 is an object and we can represent functions as object let us see the output below.

```
Called func1
```
 You can see clearly that func1 is called, now how this works is that `func1` is and object that represents the function `func1()` and we can pass it through parameters we can store them in variables and so on.

The above example is the basic principle that we need to understand in python when it comes to functions.
Now we are going to see a kind of function called **wrapper functions**.
Let us see the example below.

```
def func1(func):
   def wrapper():
       print("Started")
       func()
       print("Ended")
   return wrapper
```
Essentially, what this function is doing is that it has another function defined inside it called `wrapper`, it prints out an output called `started`, called the function `func` as we have passed into our `func1` and it will print out another value that says `Ended`. This function returns the `wrapper` function that has been defined inside our initial function.

Let's create another function called `f` and what we want to do is always do this `func1` functionality our initial function using the newly created function called `f`.

```
def func1(func):
   def wrapper():
       print("Started")
       func()
       print("Ended")
   return wrapper

def f():
    print("Hello")
func1(f)()
```
and we have the output:
```
Started
Hello
Ended
```
This might be a little bit complicated but the reason we us parenthesis () to call our function is because the value `wrapper` is a function. To understand this better, let's use the print to output the value of func1 so that you will see.

```
def func1(func):
   def wrapper():
       print("Started")
       func()
       print("Ended")
   return wrapper

def f():
    print("Hello")
print(func1(f))
```
and you can see the output:

```
<function func1.<locals>.wrapper at 0x000001C74A78ADD0>
```
so, this tells us that when you call `func1`, you have a value that is actually equal to another function `f`.
This Property is what is actually going to allow us to what is called decorate a function.

Using function aliasing, we can also have the same output as below, Let's see the example below.

```
def func1(func):
   def wrapper():
       print("Started")
       func()
       print("Ended")
   return wrapper

def f():
    print("Hello")
x = func1(f)
x()
```
output:

```
Started
Hello
Ended
```
What the above code actually does is that we have set `x` which is a variable `=` the function `func1` which has the parament `f` passed into it. So that is function aliasing, changing the name of function without changing its functionality.

You might be wondering why this article is on decorators and we are talking more about functions, well if you have understood what we have learned so far, then you pretty much understand decorators. 
So, the code above with the line `x = func1(f)` can be replaced with decorators. Let's see the code below.

```
def func1(func):
   def wrapper():
       print("Started")
       func()
       print("Ended")
   return wrapper

@func1
def f():
    print("Hello")
f()
```
What the code above does is to automatically write this line of code `x = func1(f)` for us every time we call `f` and we still have the same results.

```
Started
Hello
Ended
```
Now let's make a slide modification to our code to show you something wrong with the way we have been doing things 

```
def func1(func):
   def wrapper():
       print("Started")
       func()
       print("Ended")
   return wrapper

@func1
def f(a):
    print(a)
f("Hi")
```
and that is: notice what happens when we add a parameter to our function, and rather than printing `Hello` it will be printing `a` and we can call `f` with the value `"Hi"`

```
Traceback (most recent call last):
  File "C:\Users\Bansikah\PycharmProjects\MyCompiler\decorator.py", line 15, in <module>
    f("Hi")
TypeError: func1.<locals>.wrapper() takes 0 positional arguments but 1 was given

```
It give and error, notice that in our wrapper function.

```
def wrapper():
       print("Started")
       func()
       print("Ended")
   return wrapper
```
we call the function `func()` without any parameter, so to fix this we will use **`args`** and **`kwargs`** in the parameters of our wrapper function. You must have probably seen this in your python lesson but if you haven't then i am going to write another article on that.

```
def func1(func):
    def wrapper(*args, **kwargs):
        print("Started")
        func(*args, **kwargs)
        print("Ended")

    return wrapper


@func1
def f(a):
    print(a)
f("Hi")
```
The reason we are using the `args` and `kwargs` is that we know that our wrapper function is supposed to contain some certain number of arguments and we don't know if those arguments are `keyword` arguments or `regular in-place` arguments all we know is that it needs to have some arguments. This actually allows us to have any number of arguments on any specific decorated function and this will work properly.
Output:

```
Started
Hi
Ended
```
And we have gotten the desired output we expected, and even if we decide to add another variable as below.

```
def func1(func):
    def wrapper(*args, **kwargs):
        print("Started")
        func(*args, **kwargs)
        print("Ended")

    return wrapper


@func1
def f(a, b=9):
    print(a, b)
f("Hi")
```
output:

```
Started
Hi 9
Ended
```
this continuous to work and that is what the **`args`** and **`kwargs`** can do for us.

The final thing we are going to talk about is returning values from decorated functions, before that I will just like to say this: if the variables or function names we are using are confusing to you, you can change them to your own names that you will understand better.

So far what we have been doing is printing the values in the decorated functions, but we would like to return those values instead? Well, this is pretty much easy to do, we are going to do this by creating another function as in the example below.

```
def func1(func):
    def wrapper(*args, **kwargs):
        print("Started")
        val = func(*args, **kwargs)
        print("Ended")
        return val
    return wrapper


@func1
def f(a, b=9):
    print(a, b)


# f("Hi")

@func1
def add(x, y):
    return x + y


print(add(4, 5))
```
What we have done in the above code is that we created a function called `add` and it takes in two parameters `x` and `y` and also we created a variable `val` in the wrapper function and returned the `val`. You might be wondering that why didn't we just return the `func(*args, **kwargs)` like this: `return func(*args, **kwargs)` well it is better to do it this way.
output:

```
Started
Ended
9
```
So that is essentially how we can return values from a decorated function. In the above example you can see that you can use a decorator on more than one function.
For better understanding, let's see the applications of decorators in real world examples.

```
#Python decorators - Example 1

def before_after(func):
    def wrapper(*args):
        print("Before")
        func(*args)
        print("After")
    return wrapper
class Test:
    @before_after
    def decorated_method(self):
        print("run")
t = Test()
t.decorated_method()
```
output:

```
Before
run
After
```
Another example here, the timer decorator, a very common example maybe you have seen that before.

```
Python Decorators - Example 2
import time
def timer(func):
    def wrapper():
        before = time.time()
        func()
        print("Function took:", time.time() - before, "seconds")
    return wrapper

@timer
def run():
    time.sleep(2)
run()
```
output:

```
Function took: 2.0019941329956055 seconds
```
it measures how long a function takes to run.
And lastly the `log()` decorator example.

```


# Python Decorators - Example 3

import datetime


def log(func):
    def wrapper(*args, **kwargs):
        with open("logo.txt", "a") as f:
            f.write("Called function with" + " ".join([str(arg) for arg in args]) + "at" + str(datetime.datetime.now()) + "\n")
        val = func(*args, **kwargs)
        return val

    return wrapper


@log
def run(a, b, c=9):
    print(a + b + c)


run(1, 3, c=9)

```
output:

```
13
```
and we have this inside out `log.txt` file.

```
Called function with1 3atCalled function with1 3at2023-06-05 01:00:47.423933
Called function with1 3at2023-06-05 01:02:38.287020
```
That was it about decorators in python a very advanced and broad topic in python, congrats if you understood everything and it is really going to help you in becoming an expert in python. Please do well to ask you valuable questions in the comments section, your reactions will really encourage me to continue writing articles like this. Adios