# Decorators

In Python, a decorator is a function that takes another function as input and returns a new function as output, usually with some additional functionality. The decorator function can modify the behavior of the input function without modifying its source code.

Decorators provide a flexible and powerful way to extend the behavior of functions. They can be used to add functionality like timing, logging, validation, authorization, caching, and more. Decorators are a widely used concept in Python programming, and many built-in functions and modules use decorators.

In Python, decorators are denoted by the `@` symbol followed by the decorator function name placed above the function definition. When a decorated function is called, it is replaced by the new function returned by the decorator.&#x20;

## Creating Decorators&#x20;

In Python, there are two main ways to create decorators: function-based decorators and class-based decorators.

### Function-Based Decorators:

Function-based decorators are the simplest type of decorators. They are created using a function that takes another function as input and returns a new function as output, usually with additional functionality. Here's an example:

```python
def my_decorator(func):
    def wrapper():
        print("Before the function is called.")
        func()
        print("After the function is called.")
    return wrapper

@my_decorator
def my_function():
    print("Inside the function.")

my_function()  # Output: Before the function is called., Inside the function., After the function is called.
```

In this example, the `my_decorator` function is a decorator that takes another function (`func`) as input and returns a new function (`wrapper`) as output. The `wrapper` function adds functionality before and after calling the original function (`func`). The `@my_decorator` syntax applies the decorator to the `my_function` function, and when `my_function()` is called, it prints the output with the added functionality.

### Class-Based Decorators:

Class-based decorators are created using a class that defines a `__call__` method. The `__call__` method is called whenever the decorated function is called. Here's an example:

```python
class my_decorator:
    def __init__(self, func):
        self.func = func
    
    def __call__(self):
        print("Before the function is called.")
        self.func()
        print("After the function is called.")

@my_decorator
def my_function():
    print("Inside the function.")

my_function()  # Output: Before the function is called., Inside the function., After the function is called.
```

In this example, the `my_decorator` class is a decorator that takes another function (`func`) as input and defines a `__call__` method that adds functionality before and after calling the original function. The `__init__` method initializes the decorator with the original function. The `@my_decorator` syntax applies the decorator to the `my_function` function, and when `my_function()` is called, it prints the output with the added functionality.

Both function-based and class-based decorators are widely used in Python programming to add additional functionality to functions, methods, and classes.&#x20;

## Examples of Decorators&#x20;

Here are some examples of decorators in Python:

1. Timing Function Execution:

A common use case for decorators is to time the execution of a function. Here's an example:

```python
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Execution time: {end_time - start_time}")
        return result
    return wrapper

@timer
def some_function():
    # Function code here
    time.sleep(2)  # Simulate some time-consuming task

some_function()  # Output: Execution time: 2.0003676414489746
```

In this example, the `timer` decorator measures the execution time of the decorated function. The `wrapper` function starts the timer before calling the original function (`func`), ends the timer after the function finishes, calculates the execution time, and prints it. The `@timer` syntax applies the decorator to the `some_function` function, and when `some_function()` is called, it prints the execution time.

2. Logging Function Calls:

Another common use case for decorators is to log the calls to a function. Here's an example:

```python
def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Function {func.__name__} called with args={args}, kwargs={kwargs}")
        return func(*args, **kwargs)
    return wrapper

@logger
def some_function(x, y):
    # Function code here
    return x + y

some_function(2, 3)  # Output: Function some_function called with args=(2, 3), kwargs={}, followed by 5
```

In this example, the `logger` decorator logs the calls to the decorated function. The `wrapper` function prints the name of the function (`func.__name__`) and the arguments passed to it (`args` and `kwargs`). The `@logger` syntax applies the decorator to the `some_function` function, and when `some_function(2, 3)` is called, it prints the log message and returns the sum of `x` and `y`.

3. Checking Function Arguments:

Decorators can also be used to check the validity of the arguments passed to a function. Here's an example:

```python
def validate(func):
    def wrapper(*args, **kwargs):
        for arg in args:
            if not isinstance(arg, int):
                raise TypeError("Only integers are allowed as arguments.")
        return func(*args, **kwargs)
    return wrapper

@validate
def some_function(x, y):
    # Function code here
    return x + y

some_function(2, 3)  # Output: 5
some_function(2, "3")  # Raises TypeError: Only integers are allowed as arguments.
```

In this example, the `validate` decorator checks that all the arguments passed to the decorated function are integers. If any argument is not an integer, it raises a `TypeError`. The `@validate` syntax applies the decorator to the `some_function` function, and when `some_function(2, 3)` is called, it returns the sum of `x` and `y`. When `some_function(2, "3")` is called, it raises a `TypeError` because `"3"` is not an integer.

These are just a few examples of how decorators can be used to extend the behavior of functions in Python. With decorators, you can create reusable and customizable functionality that can be applied to any function in your codebase.&#x20;

## Chaining Multiple Decorators&#x20;

In Python, it's possible to chain multiple decorators together to apply multiple levels of functionality to a function. To chain decorators, simply apply them one after the other, from top to bottom.

Here's an example of chaining multiple decorators:

```python
def logger(func):
    def wrapper(*args, **kwargs):
        print(f"Function {func.__name__} called with args={args}, kwargs={kwargs}")
        return func(*args, **kwargs)
    return wrapper

def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Execution time: {end_time - start_time}")
        return result
    return wrapper

@logger
@timer
def some_function(x, y):
    # Function code here
    time.sleep(2)  # Simulate some time-consuming task
    return x + y

some_function(2, 3)  # Output: Function wrapper called with args=(2, 3), kwargs={}, followed by Execution time: 2.0003676414489746, followed by 5
```

In this example, the `logger` and `timer` decorators are chained together to create a new function that logs the calls to the decorated function and measures its execution time. The `@logger` decorator is applied first, followed by the `@timer` decorator. When `some_function(2, 3)` is called, it prints the log message and the execution time, and returns the sum of `x` and `y`.

It's important to note that the order of the decorators matters when chaining them together. In the example above, the `logger` decorator is applied first, which means that it wraps the `timer` decorator. This is why the log message is printed before the execution time. If you switch the order of the decorators, the behavior of the function will change accordingly.&#x20;

## Decorators with Arguments&#x20;

In Python, decorators can also take arguments, which allows you to customize their behavior. There are two types of decorators with arguments: decorators with fixed arguments and decorators with variable arguments.

1. Decorators with Fixed Arguments:

Decorators with fixed arguments take a fixed number of arguments that are passed to the decorator when it is applied to the decorated function. Here's an example:

```python
pythonCopy codedef repeat(num):
    def decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(num):
                result = func(*args, **kwargs)
            return result
        return wrapper
    return decorator

@repeat(num=3)
def say_hello(name):
    print(f"Hello, {name}!")

say_hello("John")  # Output: Hello, John!, Hello, John!, Hello, John!
```

In this example, the `repeat` decorator takes a fixed argument (`num`) that specifies the number of times the decorated function should be repeated. The `decorator` function is the actual decorator that takes the decorated function (`func`) as input and returns a new function (`wrapper`) as output. The `wrapper` function repeats the original function `num` times and returns the result. The `@repeat(num=3)` syntax applies the decorator to the `say_hello` function, and when `say_hello("John")` is called, it prints the output three times.

2. Decorators with Variable Arguments:

Decorators with variable arguments take a variable number of arguments using the `*args` and/or `**kwargs` syntax. Here's an example:

```python
def log_params(func):
    def wrapper(*args, **kwargs):
        print(f"Arguments: {args}, Keyword arguments: {kwargs}")
        return func(*args, **kwargs)
    return wrapper

@log_params
def some_function(x, y, z):
    # Function code here
    return x + y + z

some_function(1, 2, z=3)  # Output: Arguments: (1, 2), Keyword arguments: {'z': 3}, followed by 6
```

In this example, the `log_params` decorator takes a variable number of arguments using the `*args` and `**kwargs` syntax. The `wrapper` function logs the arguments and keyword arguments passed to the decorated function, and returns the result of calling the original function. The `@log_params` syntax applies the decorator to the `some_function` function, and when `some_function(1, 2, z=3)` is called, it prints the log message and returns the sum of `x`, `y`, and `z`.

Decorators with arguments are a powerful tool for creating flexible and customizable decorators in Python. By taking arguments, decorators can be adapted to a wide variety of use cases and can provide fine-grained control over the behavior of the decorated function.&#x20;

## Decorating Functions with Parameters  <a href="#decorating" id="decorating"></a>

Decorating functions with parameters can be done using decorators that take variable arguments. To decorate a function with parameters, you need to modify the decorator function to accept arguments and pass those arguments to the wrapper function that is returned by the decorator.

Here's an example of a decorator that takes an argument and can be used to decorate functions with parameters:

```python
def debug_args(debug):
    def decorator(func):
        def wrapper(*args, **kwargs):
            if debug:
                print(f"Arguments: {args}, Keyword arguments: {kwargs}")
            return func(*args, **kwargs)
        return wrapper
    return decorator

@debug_args(debug=True)
def some_function(x, y, z):
    # Function code here
    return x + y + z

some_function(1, 2, z=3)  # Output: Arguments: (1, 2), Keyword arguments: {'z': 3}, followed by 6
```

In this example, the `debug_args` decorator takes a fixed argument (`debug`) that specifies whether to print the arguments and keyword arguments passed to the decorated function. The `decorator` function takes the decorated function (`func`) as input and returns a new function (`wrapper`) as output. The `wrapper` function checks the value of `debug` and prints the arguments and keyword arguments if it is `True`. The `@debug_args(debug=True)` syntax applies the decorator to the `some_function` function, and when `some_function(1, 2, z=3)` is called, it prints the log message and returns the sum of `x`, `y`, and `z`.

By passing arguments to decorators, you can create powerful and flexible decorators that can be customized to fit a wide range of use cases.

<br>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://python-codelivly.gitbook.io/python-mastery-from-beginner-to-expert/advance-python/decorators.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
