# A Comprehensive Guide to Decorators in Python

You might have encountered functions or classes decorated with functions prefixed with `"@"`, for example, `@random`. These are known as decorators as they are placed above your class or function.

In this tutorial, you will learn about:

* Decorators in Python
    
* How to create a custom decorator
    
* The working of a decorator and how it modifies the original function
    
* How to create a custom decorator that accepts arguments and how it works
    
* Applying multiple decorators on top of a function and how they operate on the original function
    

## Decorator

**What is a decorator in Python?** A decorator is an advanced function in Python that modifies the original function without changing its source code. It offers a way to add functionality to existing functions.

If you want to create a class but don't want to write the required magic methods (such as the `__init__` method) inside it, you can use the [`@dataclass`](https://docs.python.org/3/library/dataclasses.html) decorator on top of the class, and it will take care of the rest.

```python
# @dataclass decorator example
from dataclasses import dataclass

@dataclass
class Pokemon:
    name: str
    high_power: int
    combat_power: int

    def double_hp(self):
        return self.high_power * 2

monster = Pokemon("Charizard", 200, 180)
print(monster.__dict__)

charizard_hp_doubled = monster.double_hp()
print(charizard_hp_doubled)
```

The `@dataclass` decorator adds the required magic method within the `Pokemon` class without changing the source code of the class.

This class works just like any other normal class that contains the `__init__` method in Python, and you can access its attributes, and create methods.

```bash
{'name': 'Charizard', 'high_power': 200, 'combat_power': 180}
400
```

By decorating `@dataclass` on top of the class eliminates the need to write the initializer method and other required methods within the class.

## Creating a Custom Decorator

Although there are several pre-built decorator functions available, there is also a way to design a custom decorator function for a particular task.

Consider the following simple decorator function that logs a message on the console whenever the original function is called.

```python
# Decorator function
def log_message(func):
    def wrapper():
        print(f"{func.__name__} function is called")
        func()
    return wrapper
```

The `log_message()` function is a decorator function that accepts another function (`func`) as its argument. Inside `log_message()`, a `wrapper()` function is defined which prints a message and calls `func`.

The `log_message()` returns the `wrapper`, effectively changing the behaviour of the original function (`func`).

You can now create a normal function and decorate it with the `log_message()` decorator function.

```python
# Decorator function
...

@log_message
def greet():
    print("Welcome to GeekPython")

greet()
```

Observe how the `greet()` function is decorated with the `log_message()` (`@log_message`) function. When decorating a function on top of another function, you have to stick to this convention.

When you run this code, you'll get the following result.

```bash
greet function is called
Welcome to GeekPython
```

Notice that the `greet()` function prints a simple message but the `@log_message` decorator altered the behaviour of the `greet()` function by adding a message before calling the original function. This modification happens while preserving the signature of the original function (`greet()`).

## How Decorators Work?

Look at this part of the code from the above section where you used the `@log_message` to decorate the `greet()` function.

```python
@log_message
def greet():
    print("Welcome to GeekPython")
```

The above code is equivalent to the following expression.

```python
greeting = log_message(greet)
```

You will obtain the same outcome as before if you execute the code after making the following modifications.

```python
# Decorator function
def log_message(func):
    def wrapper():
        print(f"{func.__name__} function is called")
        func()
    return wrapper

def greet():
    print("Welcome to GeekPython")

greeting = log_message(greet)
greeting()

--------------------
greet function is called
Welcome to GeekPython
```

The `greet()` function is passed to the `log_message()` function and stored inside the `greeting`. In the next line, the `greeting` is called just like any other function. What is happening and how does it work?

After this line (`greeting = log_message(greet)`) is executed, the variable `greeting` points to the `wrapper()` returned by `log_message()`. If you print the variable `greeting`, you'll get the reference of the `wrapper()` function.

```python
greeting = log_message(greet)
print(greeting)

--------------------
<function log_message.<locals>.wrapper at 0x0000024EF60C4C20>
```

This `wrapper()` function prints a message and has a reference to the `greet()` function as `func` and it calls this function within its own body to maintain the original functionality while adding extra behaviour.

## Defining Decorator Without Inner Function

One may wonder why the code in the `wrapper()` function cannot be inserted inside the scope of the `log_message()` function like in the following code.

```python
# Decorator function
def log_message(func):
    print(f"{func.__name__} function is called")
    func()
    return log_message

@log_message
def greet():
    print("Welcome to GeekPython")

greet()
```

In the above code, the code inside the `wrapper()` function is now placed within the `log_message()` function's scope. When you run the, you'll see that the `greet()` function's behaviour has changed but you get an error.

```bash
greet function is called
Welcome to GeekPython
Traceback (most recent call last):
  ...
    greet()
TypeError: log_message() missing 1 required positional argument: 'func'
```

It says one argument is missing when you called the `greet()` function which means that the `greet()` function is now pointing to the `log_message()` function. But when you simply don't call the `greet` function, it won't throw any error.

```python
...

@log_message
def greet():
    print("Welcome to GeekPython")

greet

--------------------
greet function is called
Welcome to GeekPython
```

There is little flexibility and very little you can do with it, yet in certain instances it will work.

## Handling Function Arguments Within Decorator

**What if you have a complex function that accepts arguments and processes them**, then you can't approach this problem in this way.

```python
# Decorator function
def log_message(func):
    print(f"{func.__name__} function is called")
    func()
    return log_message

@log_message
def greet(user):
    print(f"Welcome to GeekPython: {user}")

greet("Sachin")
```

This code will result in an error as the `log_message()` function doesn't have a helper function to handle the argument the `greet()` function accepts.

```bash
greet function is called
Traceback (most recent call last):
  ...
    @log_message
     ^^^^^^^^^^^
  ...
    func()
TypeError: greet() missing 1 required positional argument: 'user'
```

### Defining Decorator With Inner Function to Handle Function Arguments

You can manage the arguments received by the `greet()` function by incorporating a nested function (`wrapper()`) within the `log_message()` decorator function, using `*args` and `**kwargs` as parameters.

```python
# Decorator function
def log_message(func):
    def wrapper(*args, **kwargs):
        print(f"{func.__name__} function is called")
        func(*args, **kwargs)
    return wrapper

@log_message
def greet(user):
    print(f"Welcome to GeekPython: {user}")

greet("Sachin")

--------------------
greet function is called
Welcome to GeekPython: Sachin
```

This time, the code printed the argument (`"Sachin"`) supplied to the `greet()` function when it was called, so you didn't receive any errors.

The `*args` and `**kwargs` passed to the `wrapper()` is used to pass on the arguments to `func` (a reference for the original function) that enables the decorator function to handle the arguments accepted by the original function.

### Returning Values from Decorator

In the example above, using `greet("Sachin")` resulted in the output. However, **what if you wanted to return a value from the decorator?**

```python
@log_message
def greet(user):
    print(f"Welcome to GeekPython: {user}")
    return f"User: {user}"

# Trying to return a value
greeting = greet("Sachin")
print(greeting)
```

Since your decorator `@log_message` doesn't return a value directly, this code will return `None`.

```bash
greet function is called
Welcome to GeekPython: Sachin
None
```

To handle this situation, you need to ensure that the `wrapper()` function returns the return value of the original function.

```python
# Decorator function
def log_message(func):
    def wrapper(*args, **kwargs):
        print(f"{func.__name__} function is called")
        return func(*args, **kwargs)
    return wrapper
```

When you run the following code, you'll get the value returned by the `greet()` function.

```python
# Decorator function
def log_message(func):
    def wrapper(*args, **kwargs):
        print(f"{func.__name__} function is called")
        return func(*args, **kwargs)
    return wrapper

@log_message
def greet(user):
    print(f"Welcome to GeekPython: {user}")
    return f"User: {user}"

greeting = greet("Sachin")
print(greeting)

--------------------
greet function is called
Welcome to GeekPython: Sachin
User: Sachin
```

## Creating Decorator that Accepts Argument

So far you've created simple decorators but decorators can also accept arguments. Consider the following decorator that accepts arguments.

```python
# Decorator function to slice a string
def slice_string(start=0, end=0, step=None):
    def slice_decorator(func):
        def slice_wrapper(*args, **kwargs):
            print(f"Sliced from char {start} to char {end}.")
            if func(*args, **kwargs) == "":
                print("Text is not long enough.")
            result = func(*args, **kwargs)
            return result[start: end: step]

        return slice_wrapper

    return slice_decorator
```

In the above code, a decorator function `slice_string()` is defined. This (`slice_string()`) decorator function accepts three arguments: `start` (defaults to `0`), `end` (defaults to `0`), and `step` (defaults to `None`).

Within this (`slice_string()`) function, the inner function, `slice_decorator()`, takes another function (`func`) as an argument and within the `slice_decorator()` function, a wrapper function (`slice_wrapper()`) is defined.

The `slice_wrapper()` function takes any positional (`*args`) and keyword (`**kwargs`) arguments required to handle arguments if any accepted by the original function.

The `slice_wrapper()` function prints a simple message, and in the next line, checks if the argument is an empty string, if it is then a message is printed otherwise, the result is sliced from the specified range.

This `slice_wrapper()` function is returned by the `slice_decorator()` function and eventually, the `slice_decorator()` function is returned by the `slice_string()` function.

Now you can create a function and decorate `@slice_string` on top of it.

```python
# Decorator function to slice a string
...

@slice_string(2, 7)
def intro(text):
    return text
```

The `intro()` function is defined that takes `text` as an argument and returns it. Two arguments (`2` and `7`) are passed to the `@slice_string` decorator, meaning the `text` will be sliced from the character at index 2 to index 7 (excluding the character at the 7th index).

```python
# Decorator function to slice a string
...

chars = intro("Welcome to GeekPython")
print(chars)

--------------------
Sliced from char 2 to char 7.
lcome
```

Overall, a decorator function that accepts arguments typically involves the interaction of three functions: the outer function (the decorator itself) that accepts arguments, an inner function (the wrapper) that receives the original function, and a nested function (the innermost wrapper) that modifies the behaviour of the original function.

Here is another example of a decorator that accepts an argument.

```python
import time

def sleep_code(t):
    def sleep_decorator(func):
        def sleep_wrapper(*args, **kwargs):

            # Calculate start time
            start = time.perf_counter()
            print(f"Execution Delayed: {t} Seconds")
            # Sleep for t seconds
            time.sleep(t)
            # Calculate end time
            end = time.perf_counter()
            # Evaluate execution time
            print(f"Execution Took   : {round(end - start)} Seconds")

            return func(*args, **kwargs)
        return sleep_wrapper
    return sleep_decorator

@sleep_code(5)
def slow_down(x, y):
    return x**y

obj = slow_down(2, 3)
print(obj)
```

The `@sleep_code` decorator takes an argument `t` representing time in seconds. It modifies the behaviour of the original function (`slow_down()`) by delaying its execution using `time.sleep(t)` within the innermost function (`sleep_wrapper()`). Additionally, before returning the result, it prints the execution time taken by the code, which is measured using `time.perf_counter()`.

When you run the code, you'll get the following result.

```bash
Execution Delayed: 5 Seconds
Execution Took   : 5 Seconds
8
```

## Stacking Multiple Decorators on Top of a Function

So far you might have a pretty good idea about decorators and in this section, you'll see that multiple decorator functions can be stacked on top of another function. Here's a simple example.

```python
# First decorator function
def decorator__1(func):
    def wrapper_d1(*args, **kwargs):
        print(f"Called decorator 1")
        return func(*args, **kwargs)
    return wrapper_d1

# Second decorator function
def decorator_2(func):
    def wrapper_d2(*args, **kwargs):
        print(f"Called decorator 2")
        return func(*args, **kwargs)
    return wrapper_d2

# Decorated with multiple decorators
@decorator_1
@decorator_2
def log_message():
    return "Message logged"

message = log_message()
print(message)
```

Both `decorator_1()` and `decorator_2()` have the same boilerplate and log a simple message.

The `log_message()` is decorated with both (`@decorator_1` and `@decorator_2`) decorators with the `@decorator_1` being on the topmost level followed by the `@decorator_2`.

When you run this code, you'll get the following result.

```bash
Called decorator 1
Called decorator 2
Message logged
```

You can see that messages logged by the decorators are in the exact order as they are stacked on top of the `log_message()` function.

If you reverse the order of these decorators, the messages will be logged in the same order as well.

```python
# Reversed the order of the decorators
@decorator_2
@decorator_1
def log_message():
    return "Message logged"

message = log_message()
print(message)

--------------------
Called decorator 2
Called decorator 1
Message logged
```

The code is equivalent to passing `log_message()` through `decorator_1()` first, and then passing the result (`decorator_1(log_message)`) through `decorator_2()`.

```bash
message = decorator_2(decorator_1(log_message)
```

> **Note: When you are stacking multiple decorators on top of the function, their order matters.**

### Practical Example

Here's an example that shows when decorating a function with multiple decorators, they need to be in order.

```python
@slice_string(2, 7)
@sleep_code(2)
def intro(text):
    return text

chars = intro("Welcome to GeekPython")
print(chars)
```

When you run this code, the execution will delayed for 4 seconds because the `sleep_code()` will be invoked twice.

```bash
Sliced from char 2 to char 7.
Execution Delayed: 2 Seconds
Execution Took   : 2 Seconds
Execution Delayed: 2 Seconds
Execution Took   : 2 Seconds
lcome
```

If you just reverse the order of the decorators in the above code, that would just work fine.

```python
@sleep_code(2)
@slice_string(2, 7)
def intro(text):
    return text

chars = intro("Welcome to GeekPython")
print(chars)
```

**Output**

```bash
Execution Delayed: 2 Seconds
Execution Took   : 2 Seconds
Sliced from char 2 to char 7.
lcome
```

You can observe the difference in the output in which the execution of the code took only 2 seconds. That's why you need to ensure that the decorators are in the correct order above the function.

## Conclusion

**Decorators** modify the behaviour of the original function without changing the source code of the original function. They are advanced functions that do modification while preserving the original function's signature.

Python has several built-in decorator functions, and you can also create the custom decorator your program may need.

You saw when you define a custom decorator, you create a function returning a wrapper function. This wrapper function handles the modification and if your decorated function accepts arguments then it uses `*args` and `**kwargs` to pass on arguments. If the decorator function accepts arguments then you end up nesting the wrapper function into another function.

You also observed that in order to get the appropriate outcome, decorators must be stacked correctly on top of any function.

---

🏆**Other articles you might be interested in if you liked this one**

✅[Why if \_\_name\_\_ == "\_\_main\_\_" is used in Python programs](https://geekpython.in/understanding-if-__name__-__main__-in-python-programs)?

✅[Serialize and deserialize Python objects using the pickle module](https://geekpython.in/pickle-module-in-python).

✅[Create a WebSocket server and client in Python](https://geekpython.in/build-websocket-server-and-client-using-python).

✅[Create and integrate MySQL database with Flask app using Python](https://geekpython.in/create-and-integrate-mysql-database-with-flask-app).

✅[What is \_\_getitem\_\_ method in Python class](https://geekpython.in/python-getitem-method)?

✅[What is the yield keyword in Python and how it is different from the return keyword](https://geekpython.in/yield-keyword-in-python)?

---

**That's all for now**

**Keep Coding✌✌**
