Python Decorators
Tags: python, webdev, beginners
Published: 2023-03-17 13:49:38

Python is a high-level programming language that offers a lot of flexibility to programmers. One of the features that make Python such a versatile language is decorators. Decorators are functions that modify other functions or classes, and they provide a way to add functionality to your code without changing the original code.

In this blog post, we will explore what Python decorators are, how they work, and some common use cases.

## What are Python decorators?

In Python, a decorator is a function or class that takes a function as its argument and returns a new function or class, respectively. The new function or class is usually a modified version of the original function that adds some additional functionality. For the rest of the blog I will only be referring to function decorators, however please note that class decorators exist, can be called in the same way, and can extend class functionality without modifying the class. The decorator function is called before the original function and can modify the inputs, outputs, or behavior of the original function.

The syntax for a decorator is as follows:

```
@decorator_function
def my_function():
    pass

```

The @decorator_function syntax is known as the decorator syntax. It tells Python to pass the function my_function as an argument to the decorator_function.

## Types of Python decorators

There are two types of decorators in Python: function decorators and class decorators.

**1. Function decorators**

Function decorators are the most common type of decorators in Python. They are used to modify the behavior of a function.

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

@my_decorator
def my_function():
    print("Hello, World!")

my_function()

```

In this example, my_decorator is a function that takes another function func as an argument and returns a new function wrapper. wrapper adds some additional functionality before and after func is called. The @my_decorator syntax tells Python to pass my_function as an argument to my_decorator and use the resulting function as the new definition of my_function.

**2. Class decorators**

Class decorators are used to modify the behavior of a class.

```
def my_class_decorator(cls):
    class NewClass:
        def __init__(self, *args, **kwargs):
            self.obj = cls(*args, **kwargs)

        def __getattr__(self, name):
            return getattr(self.obj, name)

        def __setattr__(self, name, value):
            setattr(self.obj, name, value)

    return NewClass

@my_class_decorator
class MyClass:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def my_method(self):
        return self.x + self.y

obj = MyClass(3, 4)
print(obj.my_method())

```

In this example, my_class_decorator is a function that takes a class cls as an argument and returns a new class NewClass. NewClass modifies the behavior of cls by intercepting attribute access and delegation to the original cls object. The @my_class_decorator syntax tells Python to use my_class_decorator to modify the behavior of MyClass.

## Uses of Python decorators

Python decorators can be used for a wide range of purposes, including:

**1. Logging**
Decorators can be used to log the input and output of a function.

```
def log(func):
    def wrapper(*args, **kwargs):
        print(f"Calling function {func.__name__} with arguments {args} and keyword arguments {kwargs}")
        result = func(*args, **kwargs)
        print(f"Function {func.__name__} returned {result}")
        return result
    return wrapper

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

add(3, 4)

```

In this example, the log decorator is used to log the input and output of the add method. The log decorator takes a function func as an argument and returns a new function wrapper that logs the input and output of func. The wrapper takes any number of positional and keyword arguments by using *args and **kwargs respectively. To use the log decorator simply add '@log' on the line before your function declaration.


**2. Timing**
Decorators can also be used to time the execution of a function.

```
import time

def timer(func):
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()
        print(f"Function {func.__name__} took {end_time - start_time:.2f} seconds to execute.")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(2)

slow_function()

```

In this example, the timer decorator is used to time the execution of the slow_function. The decorator measures the time taken to execute the function and prints the elapsed time.

**3. Authorization**
Decorators can be used to restrict access to certain functions or classes based on authorization rules.

```
def authorized(func):
    def wrapper(*args, **kwargs):
        if is_authorized():
            return func(*args, **kwargs)
        else:
            raise Exception("You are not authorized to access this function.")
    return wrapper

@authorized
def secure_function():
    pass

secure_function()

```

In this example, the authorized decorator is used to restrict access to the secure_function. The decorator checks if the user is authorized to access the function and raises an exception if the user is not authorized.

**4. Caching**
Decorators can be used to cache the results of expensive function calls.

```
def cache(func):
    results = {}
    def wrapper(*args):
        if args in results:
            print(f"Returning cached result for {args}")
            return results[args]
        result = func(*args)
        results[args] = result
        print(f"Caching result for {args}")
        return result
    return wrapper
```

In this example, cache uses a wrapper to capture and store the results of func. The wrapper function first checks if the function has already been called with the same arguments by checking if the args are already in the results dictionary. If the args are in the results dictionary, the wrapper function returns the cached result and logs a message indicating that the result is being returned from the cache.

This is only a short list of what can be done with python decorators! The sky is truly the limit when you can call a function to modify another one. Some other useful implementations of decorators might include retry, validation, debugging, etc. If you're curious to dive into a deep but incredibly useful topic maybe try starting your journey [here.](https://www.geeksforgeeks.org/python-decorators-a-complete-guide/)