Tutorial

Decorators in Python

6 min read

In Python, functions are first-class objects, which means they can be assigned to variables, passed as arguments to other functions, and returned as values from other functions. This property enables the use of decorators in Python, which essentially wraps a function with another function, allowing you to perform additional actions before or after the wrapped function is called.

The syntax for using decorators involves using the “@” symbol followed by the name of the decorator function above the function definition. When the decorated function is called, the decorator function is invoked first, and it can modify the original function’s behavior or perform any other desired actions.

Python Decorators can be used for a variety of purposes, such as adding logging, input validation, authentication, caching, and more. They provide a clean and concise way to separate cross-cutting concerns from the core logic of a function or class. Python provides built-in decorators like “@property” and “@staticmethod” which are commonly used in object-oriented programming. However, decorators can also be defined by the user, allowing for custom functionality to be added to functions or classes.

Python Decorators with Examples

Python Decorators provide a clean and efficient way to add functionality, such as logging, caching, input validation, or authentication, to existing code.

Let’s explore some examples to understand how decorators work and how they can be applied.

  • Logging Decorator:  Suppose you have multiple functions in your codebase and want to add logging statements to track when each function is called and its return value.

Here’s an example of a logging decorator that achieves this:

def log_decorator(func):
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args: {args}, kwargs: {kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned: {result}")
        return result
    return wrapper

@log_decorator
def add(a, b):
    return a + b

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

# Function calls with logging
print(add(2, 3))           # Output: Calling add with args: (2, 3), kwargs: {}, add returned: 5
print(multiply(4, 5))      # Output: Calling multiply with args: (4, 5), kwargs: {}, multiply returned: 20

Here, the log_decorator function takes a function as an argument, wraps it with additional logging statements, and returns the wrapped function. By applying the @log_decorator above the function definitions, the functions add and multiply are automatically decorated with the logging behavior.

  • Timing Decorator: Another common use case for decorators is measuring the execution time of a function. Here’s an example of a timing decorator:
import time

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

@time_decorator
def slow_function():
    time.sleep(3)
    return "Function executed."

# Function call with timing
print(slow_function())     # Output: slow_function took 3.001234 seconds to execute.

Here, the time_decorator wraps the function slow_function with timing logic. It measures the time taken to execute the function and prints the duration. The @time_decorator notation applies the timing behavior to the slow_function.

@ Symbol With Decorator in Python

In Python, the “@” symbol is used to apply a decorator to a function or class.

It is syntactic sugar that simplifies the process of decorating a target function or class with a decorator function.

When the “@” symbol is placed before the name of a decorator function and followed by the target function or class, it signifies that the decorator should be applied to the target.

The “@” symbol essentially serves as a shortcut for calling the decorator function and passing the target as an argument.

Example of @ Symbol With Decorator in Python-

def my_decorator(func):
    def wrapper():
        print("Before function execution")
        func()
        print("After function execution")
    return wrapper

@my_decorator
def my_function():
    print("Inside my_function")

# Calling the decorated function
my_function()

Here, the my_decorator function is defined as a decorator. It takes a function as an argument, wraps it with additional functionality, and returns the wrapped function.

The decorator adds a “Before function execution” message before calling the target function and an “After function execution” message after executing the target.

By using the “@” symbol and applying the @my_decorator above the my_function definition, the decorator is automatically applied to the function. So, when my_function is called, it is actually the decorated version that gets executed.

Output:

Before function execution
Inside my_function
After function execution

Decorating Functions with Parameters

Decorating functions with parameters in Python involves handling both the arguments passed to the original function and any additional arguments that might be needed by the decorator itself. This can be achieved by using *args and **kwargs to capture the arguments dynamically.

Example of decorating Functions with Parameters-

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before function execution")
        result = func(*args, **kwargs)
        print("After function execution")
        return result
    return wrapper

@my_decorator
def add_numbers(a, b):
    return a + b

# Calling the decorated function with parameters
print(add_numbers(2, 3))

Here, the my_decorator function is defined as a decorator.

The wrapper function inside the decorator is designed to handle any number of arguments and keyword arguments.

It first executes the additional code before calling the target function and then executes the additional code after the target function is executed.

Finally, it returns the result of the target function. The *args and **kwargs parameters in the wrapper function allows the decorator to capture and pass any number of arguments and keyword arguments to the target function, regardless of their names or values.

This ensures that the decorated function can still accept and process the necessary parameters.

Output:

Before function execution
After function execution
5

By using *args and **kwargs in the wrapper function, decorators can be applied to functions with any number and type of parameters, making the decoration process more flexible and accommodating different use cases.

It allows us to modify the behavior of functions while preserving their original parameter-handling functionality.

Chaining Decorators in Python

Chaining decorators in Python involves applying multiple decorators to a single function in a sequential manner.

This allows you to layer multiple functionalities onto a function by applying multiple decorators, each with its own specific behavior.

Chaining decorators can be done using the “@” symbol multiple times before the function definition.

Example Chaining Decorators in Python-

def uppercase_decorator(func):
    def wrapper():
        result = func().upper()
        return result
    return wrapper

def emphasis_decorator(func):
    def wrapper():
        result = func()
        return f"**{result}**"
    return wrapper

@uppercase_decorator
@emphasis_decorator
def greet():
    return "hello"

# Calling the decorated function
print(greet())

Here, we have two decorators: uppercase_decorator and emphasis_decorator. The uppercase_decorator converts the result of the function to uppercase, while the emphasis_decorator wraps the result with double asterisks.

By using the “@” symbol twice before the function definition and applying the decorators in the order @uppercase_decorator and @emphasis_decorator, the decorators are chained together. This means that the output of the uppercase_decorator becomes the input for the emphasis_decorator.

When the greet function is called, it executes the decorated version of the function. The execution starts with the emphasis_decorator, which wraps the result of the uppercase_decorator with double asterisks. Finally, the modified result is returned.

Output:

**HELLO**

Chaining decorators allow us to stack multiple layers of functionality onto a function, each modifying the behavior or result in a specific way. It provides a convenient and expressive way to combine and compose different decorators to achieve the desired functionality.

When chaining decorators, the order in which the decorators are applied is significant. The decorators closer to the function definition are applied first, followed by the decorators further away.

So, make sure to consider the order in which we chain the decorators to achieve the desired effect.


Leave a Reply

Your email address will not be published. Required fields are marked *