πŸ€–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.

Creating Decorators

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:

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:

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.

Examples of Decorators

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:

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.

  1. Logging Function Calls:

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

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.

  1. Checking Function Arguments:

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

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.

Chaining Multiple Decorators

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:

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.

Decorators with Arguments

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:

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.

  1. 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:

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.

Decorating Functions with Parameters

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:

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.

Last updated