Python Decorators
Tags: python, pythondecorators, advanced
Published: 2023-08-03 08:47:34

Python is an interpreted, object-oriented, high-level programming language.

Decorators are function that allows users to add new functionality to an existing object without changing it's structure. 

## Prerequisites

1. Basic understanding of python functions.
2. **args and **kwargs

Python functions are defined as <u>first class objects</u>. This means that the python functions can be assigned to variables, pass them as arguments to other functions, nested in other functions, and return them as values in other functions.

To well grasp python decorators we will dive in first to this concept of functions as first class objects.

## 1. Assigning functions to variables
Here we will demonstrate this concept by creating a function that greets a user with a "Hello World" message.

```python
def greet():
    return "Hello World"
message = greet
print(message())
```

The output of the above code is `Hello World`

## 2. Pass function as an argument
We will illustrate by creating two functions. The greet function will be returning the string `Hello World` and the call function will be accepting a function as an argument then return the argument.
```python
def greet():
    return "Hello World"

def call_function(function):
    return function()

print(call_function(greet))
```

The output for the above code is `Hello World`

## 3. Nest a function
Python also allows for nested functions. This concept is crucial in decorators.
```python
def main():
    def greet():
        return "Hello World"
    return greet()

print(main())
```

The output if the above function is `Hello World`

## 4. Returning functions in other functions
A function can also be generated by another function.
```python
def main():
    def say_hello():
        return "Hello"
    return say_hello
main_func = main()
print(main_func())
```
The output is `Hello`

## Creating decorators

Having gone through the concept of functions as first class objects, we will now create our first decorator. A python decorator is much similar to nested function. For this we will create two functions, an inner function and an outer function, where the inner function is nested inside the outer function.

```python
def make_lowercase(function):
    def wrapper():
        func = function()
        return func.lower()
    return wrapper
```
We can clearly see that our decorator takes a function as an argument. We shall now create a function and pass it to our decorator function.

```python
def say_hello():
    return "HELLO WORLD"

decorator = make_lowercase(say_hello)
print(decorator())
```
The output is `hello world`

Python also provides another way to assign decorators to functions. We can use the @ symbol followed by the name of the decorator.

```python
@make_lowercase
def say_hello():
    return "HELLO WORLD"

print(say_hello())
```
The output is `Hello World`

## Using multiple decorators
We can also apply multiple decorators in our python functions. For example let's say that we want also to delay our function execution by 5 seconds. Then we will define a decorator function for delaying function execution as follows.

```python
import time
def delay(function):
    def wrapper():
        time.sleep(5)
        return function()
    return wrapper
```

```python
@delay
@make_lowercase
def say_hello():
    return "HELLO WORLD"

print(say_hello())
```
The output still remains `Hello World` but the execution of the function say_hello is delayed by 5 seconds.

## Defining decorator functions with arguments
We can also define a decorator function that accepts arguments. We will do this by defining arguments in the wrapper function, where this arguments will be passed to the function being decorated at run time.

```python
def make_title(function):
    def wrapper(arg1):
        function(arg1.title())
    return wrapper

@make_title
def book(title):
    print(f"The title of the book is {title}")

book("broken glass")
```
The output of the above snippet is `The title of the book is Broken Glass` where the argument python is in title format.

## Decorator functions with *args and **kwargs
We can define a decorator function that accepts *args and **kwargs. The *args stores all the positional arguments and the **kwargs stores all the keyword arguments.

```python
def decorate(function):
    def wrapper(*args, **kwargs):
        print(f"The args are {args}")
        print(f"The kwargs are {kwargs}")
        function(*args, **kwargs)
    return wrapper

@decorate
def function_with_no_arguments():
    print("Has no arguments")

function_with_no_arguments()
```
We have defined a decorator that accepts arguments and passes the arguments to the decorated function. We have also defined a function named function_with_no_arguments which accepts no arguments and decorated it with the decorator function. The output of the function is 
```
The args are ()
The kwargs are {}
Has no arguments
```

Let now decorate a function with positional arguments.
```python
@decorate
def function_with_arguments(arg1, arg2):
    print("This function has arguments")

function_with_arguments("Python", "Java")
```

The output of the function_with_arguments is:
```
The args are ('Python', 'Java')
The kwargs are {}
This function has arguments
```
We can see that the args Python and Java are printed to the console.

Let's see how a function with keyword arguments will look like.
```python
@decorate
def function_with_kwargs(**kwargs):
    print("Has kwargs")

function_with_kwargs(car="Subaru", model="SUBARU CROSSTREK SUV 2021")
```

The output of the function_with_arguments is:
```
The args are ()
The kwargs are {'car': 'Subaru', 'model': 'SUBARU CROSSTREK SUV 2021'}
Has kwargs
```
We can see that the args Python and Java are printed to the console.

## Conclusion
We have seen how define function as first class objects in python. Also we have seen how to define decorators in python. Decorator in python help in observing the DRY concept in python, that is Don't Repeat Yourself. Decorators in python has various use cases like:
1. Caching using the inbuilt @lru_cache
2. Used in frameworks such as django, flask and fastapi