How to Use Decorators in Python?
Tags: python, programming, beginners
Published: 2023-03-05 02:33:23

Decorators in Python are a powerful tool that allows you to modify the behavior of functions and classes. In this tutorial, we'll take a deeper look at advanced decorators and how they can be used to solve common programming challenges.

This tutorial will talk about the different kinds of decorators, decorator chaining, and some common useful decorators

## Decorator Functions

A decorator function takes a function as an argument and returns a modified version of it. It is used to modify the behavior of the original function.

Here's an example of a simple decorator function

```python
# The decorator function adds two numbers to the result of the original function
def add_two(func):
    def wrapper(*args, **kwargs):
        # Original Function is invoked
        result = func(*args, **kwargs)
        # Result of original function is modified by adding 2
        return result + 2
    return wrapper

@add_two
def multiply(a, b):
    return a * b

@add_two
def divide(a,b):
  return int(a/b)

print(multiply(3, 4)) # 14
print(divide(10,2)) #7
```

In this example, the `add_two` decorator function takes the `multiply` function as an argument and returns the `wrapper` function, which adds two to the result of `multiply`. The `@add_two` syntax before the `multiply` function is a shorthand for the following:

```python
def multiply(a, b):
    return a * b

multiply = add_two(multiply)
```

Since we do not know the number of parameters the original function might have [we use `args` and `kwargs`](https://www.realpythonproject.com/day16-positional-arguments-keyword-arguments-and-the-terms-args-and-kwargs/)

## Decorator Classes

In addition to decorator functions, you can also use decorator classes to modify the behavior of functions and classes.

Here's an example of a simple decorator class that adds two numbers to the result of the original function:

```python
class AddTwo:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        result = self.func(*args, **kwargs)
        return result + 2

@AddTwo
def multiply(a, b):
    return a * b

@AddTwo
def divide(a,b):
  return int(a/b)

print(multiply(3, 4)) # 14
print(divide(10,2)) #7
```

`__init__` and `__call__` are called [dunder methods or magic methods](https://www.realpythonproject.com/python-magic-oop-dunder/)

In this example, the `AddTwo` decorator class takes the `multiply` function as an argument and returns a callable object (an instance of `AddTwo`) that adds two to the result of `multiply`. The `@AddTwo` syntax before the `multiply` function is a shorthand for the following

```python
def multiply(a, b):
    return a * b

multiply = AddTwo(multiply)
```

## Parametrized Decorators

So far, we've looked at simple decorators that modify the behavior of a function in a fixed way. However, you can also create parametrized decorators that take parameters to control their behavior.

Here's an example of a parametrized decorator function that adds a specified number to the result of the original function:

```python
def add_n(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            return result + n
        return wrapper
    return decorator

@add_n(10)
def multiply(a, b):
    return a * b

@add_n(12)
def divide(a,b):
  return int(a/b)

print(multiply(3, 4)) # 22
print(divide(10,2)) # 17
```

In this example, the `add_n` decorator function takes a parameter `n` and returns a decorator function `decorator` that adds `n` to the result of the original function. The `@add_n(10)` syntax before the `multiply` function is a shorthand for the following:

```python
def multiply(a, b):
    return a * b

multiply = add_n(10)(multiply)
```

Below is an image comparing our first decorator `add_two` and the parameterized decorator `add_n`

![Decorators with parameters in Python](https://www.realpythonproject.com/wp-content/uploads/2023/03/Screen-Shot-2023-03-04-at-5.10.56-PM-1.png align="left")

* Before we were wrapping the original function and returning the wrapper
    
* Now, we are wrapping the decorator (that wraps the original function) and returning the decorator
    

## Decorating Classes

In our previous examples, we looked at how to decorate functions. But you can decorate classes as well

Here's an example of a simple class decorator that adds a class attribute `n` to the decorated class:

```python
def add_attribute(n):
    def class_decorator(cls):
        cls.n = n
        return cls
    return class_decorator

@add_attribute(10)
class MyClass:
    pass

print(MyClass.n) # 10
```

In this example, the `add_attribute` decorator function takes a parameter `n` and returns a class decorator function `class_decorator` that adds the class attribute `n` to the decorated class. The `@add_attribute(10)` syntax before the `MyClass` class is a shorthand for the following:

```Python
class MyClass:
    pass

MyClass = add_attribute(10)(MyClass)
```

Below is another example fo decorating classes

```Python
def add_method(cls):
    def new_method(self):
        return "Hello, World!"
    setattr(cls, 'new_method', new_method)
    return cls

@add_method
class MyClass:
    pass

my_obj = MyClass()
print(my_obj.new_method()) # Hello, World
```

It simply adds `new_method` to every instance of `MyClass`

## Decorator Chaining

One of the most powerful features of decorators is the ability to chain them together to modify the behavior of a function or class in multiple ways. Remember, a decorator simply returns a modified function. This modified function can be passed to another decorator to further modify it.

Here's an example of how to chain two decorators to modify the behavior of a function:

```Python
# Adds n to the result of original function
def add_n(n):
    def decorator(func):
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            return result + n
        return wrapper
    return decorator

# Multiplies result of original function by n
def multiply_by_m(m):
    def decorator(func):
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            return result * m
        return wrapper
    return decorator

@add_n(10)
@multiply_by_m(5)
def multiply(a, b):
    return a * b

@multiply_by_m(5)
@add_n(12)
def divide(a,b):
  return int(a/b)

print(multiply(3, 4)) # 70
print(divide(10, 2)) #85
```

We have two parameterized decorators to multiply and add. The order of applications of the decorators chained is from bottom to top. I.e the last decorator is applied first and the first decorator is applied last.

In this example, the `multiply` function is decorated by both the `add_n(10)` and `multiply_by_m(5)` decorators. The `multiply_by_m(5)` decorator is applied first, followed by the `add_n(10)` decorator.

In the second case, however, The `add_n(12)` decorator is applied first and `multiply_by_m(5)` is applied second.

## Use Cases of Decorators

### 1\. Timing Decorator

A timing decorator can be used to measure the time taken by a function to execute. Here's an example:

```python
import time

def timing_decorator(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"Time taken: {end - start:.2f} seconds")
        return result
    return wrapper

@timing_decorator
def long_running_function():
    time.sleep(3)

long_running_function()
```

In this example, the `timing_decorator` decorator adds timing information to the `long_running_function` function. When the `long_running_function` is executed, the `wrapper` function provided by the decorator measures the time taken for the function to execute and prints the result.

### 2\. Authorization Decorator

An authorization decorator can be used to enforce access control on a function. Here's an example:

```Python
def authorized(func):
    def wrapper(*args, **kwargs):
        user = get_current_user()
        if not user.is_authenticated():
            raise Exception("Unauthorized")
        return func(*args, **kwargs)
    return wrapper

@authorized
def restricted_function():
    print("Access granted")
```

In this example, the `authorized` decorator adds an authorization check to the `restricted_function` function. When the `restricted_function` is executed, the `wrapper` function provided by the decorator retrieves the current user and checks if the user is authenticated. If the user is not authenticated, the wrapper raises an exception.

### 3\. Logging Decorator

A logging decorator can be used to log the execution of a function. Here's an example:

```python
def logging_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Calling function: {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Returning from function: {func.__name__}")
        return result
    return wrapper

@logging_decorator
def some_function(a, b):
    return a + b

print(some_function(3, 4)) # 7
```

In this example, the `logging_decorator` decorator adds logging information to the `some_function` function. When the `some_function` is executed, the `wrapper` function provided by the decorator logs the function name before and after the function is executed.

We can chain this with the timing decorator

```Python
@logging_decorator
@timing_decorator
def long_running_function():
    time.sleep(3)

'''
Calling function: wrapper
Time taken: 3.00 seconds
Returning from function: wrapper
'''
long_running_function()
```

### 4\. Cache Decorator

A cache decorator can be used to cache the results of a function for faster access. Here's an example:

```Python
def cache_decorator(func):
    cache = {}
    def wrapper(*args, **kwargs):
        key = f"{args[0]}"
        print(cache)
        if key not in cache:
            print("Calling function")
            cache[key] = func(*args, **kwargs)
        else:
            print("Getting value from cache")
        return cache[key]
    return wrapper

@cache_decorator
def expensive_function(n):
    result = 0
    for i in range(n):
        result += i
    return result

'''
{}
Calling function
4950
{'100': 4950}
Getting value from cache
4950
'''
print(expensive_function(100)) # 4950
print(expensive_function(100)) # 4950
```

In this example, the `cache_decorator` decorator adds a cache to the `expensive_function` function. When the `expensive_function` is executed, the `wrapper` function provided by the decorator checks if the result is already present in the cache. If it is, the cached result is returned, otherwise the function is executed and the result is added to the cache.

Notice how the `cache` a dictionary is still able to persist between calls to the decorated function because the wrapper function created by the decorator retains a reference to the `cache` dictionary. This allows the wrapper function to access and modify the `cache` dictionary.

If a new function is decorated with the `cache` decorator, the wrapper function will have a reference to a new empty `cache` dictionary

```Python
'''
{}
Calling function
100
'''
print(outputResult(100)) 

'''
{'100': 4950}
Getting value from cache
4950
'''
print(expensive_function(100))
```

### 5\. Singleton Class Decorator

A singleton class decorator can be used to ensure that only one instance of a class is created. Here's an example:

```Python
def singleton(cls):
    instances = {}
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class MyClass:
    pass

obj1 = MyClass()
obj2 = MyClass()
obj3 = MyClass()
print(obj1 is obj2) # True
print(obj2 is obj3) # True
print(obj1 is obj3) # True
```

In this example, the `singleton` decorator is used to decorate the `MyClass` class. The decorator works by creating a `get_instance` function that stores an instance of the class in a dictionary. When the `get_instance` function is called, it checks if an instance of the class already exists and returns it if it does, otherwise, it creates a new instance of the class and adds it to the dictionary.

## Conclusion

In conclusion, decorators are a powerful feature in Python that allow you to add functionality to a function without modifying its original code. Decorators can be used to add caching, logging, validation, and other functionality to functions in a modular and reusable way.

In this tutorial, we learned how to define and use decorators in Python, including how to define decorator functions and decorator classes, and how to use decorators to add caching to a function.

If you'd like a more in-depth tutorial about decorators, you should watch [this](https://youtu.be/MjHpMCIvwsY) awesome Pycon Talk

Overall, decorators are a valuable tool in your Python toolbox, and mastering decorators can help you write more modular, reusable, and maintainable code

---

This article was originally published on [RealPythonProject.com](https://www.realpythonproject.com/how-to-use-decorators-in-python/) on March 4th,2023.