Decorators in Python with Examples

Python

Decorators in Python are a powerful feature that allows us to modify the behavior of functions or classes. They allow us to wrap another function in order to extend the behavior of the wrapped function, without permanently modifying it. In this article, we will explore decorators and their usage for profiling and logging.

Table of Contents

What is a Decorator?

A decorator is a callable that takes another function as an argument and returns yet another function. The returned function typically wraps the input function with some additional behavior. This allows us to easily add functionality to existing functions without modifying them directly.

Use Cases of Decorators

Decorators are used for a variety of tasks such as:

  1. Authentication: You can use a decorator to check if a user is authenticated before allowing them to access certain views in your web application.
  2. Logging: A decorator can be used to log function calls, timing information, and other useful debug information.
  3. Caching: Decorators can be used to cache the results of expensive function calls and return the cached result when the same inputs occur again.
  4. Validation: You can use a decorator to validate function arguments before they are passed to the decorated function.
  5. Modifying Function Behavior: A decorator can modify the behavior of a function without changing its source code, which makes it easier to maintain and understand your code.

Python Decorator Example

In Python, we use the @ symbol followed by the name of the decorator before a function definition to apply it to that function. For example:


def custom_decorator(func):
    def wrapper(*args, **kwargs):
        print("Before the function is called, something happens.")
        func(*args, **kwargs)
        print("After the function is called, something happens.")
    return wrapper

@custom_decorator
def say_hello(name):
    print(f"Hello {name}")
    
say_hello("Developer")

In this example, say_hello is decorated with custom_decorator. When we call say_hello(), it will first print "Before the function is called, something happens.", then call the original say_hello function, and finally print "After the function is called, something happens."

Output:

Something is happening before the function is called.
Hello, world
Something is happening after the function is called.

Python Decorators with Parameters

In the following code example, a custom decorator named custom_decorator_with_params is defined. This decorator accepts arguments, allowing for customization of its behavior. Within this decorator, there’s an inner function called inner, which acts as the actual decorator. Inside inner, another function named wrapper is defined. This wrapper function serves as a wrapper around the original function that will be decorated.

If the decorator specifies the upper_case parameter and it’s set to True, string arguments are transformed to uppercase before calling the original function.

The say_hello function is then decorated using @custom_decorator_with_params(upper_case=True), which means that the upper_case parameter is set to True for this specific decoration. Finally, the decorated say_hello function is invoked with the argument "Developer".


# Define a decorator function that accepts parameters
def custom_decorator_with_params(*dec_args, **dec_kwargs):
    # Define an inner function that will be returned as the actual decorator
    def inner(func):
        # Define a wrapper function that will wrap the original function
        def wrapper(*args, **kwargs):
            # Check if the decorator has specified 'upper_case' parameter and it's True
            if "upper_case" in dec_kwargs and dec_kwargs['upper_case'] == True:
                # If True, transform any string arguments to uppercase
                transformed_args = tuple(item.upper() if isinstance(item, str) else item for item in args)
                # Call the original function with transformed arguments
                func(*transformed_args, **kwargs)
            else:
                # If 'upper_case' parameter is not specified or it's False
                func(*args, **kwargs)
            
        return wrapper
    return inner

@custom_decorator_with_params(upper_case=True)
def say_hello(name):
    # Original function body
    print(f"Hello {name}.")

# Call the decorated function
say_hello("Developer")

Output:

Hello DEVELOPER.

Profiling with Decorators

Profiling is a way to measure how long different parts of your code take to execute. Python provides a built-in module for this purpose: cProfile. We can use decorators to easily add profiling to any function. Here’s an example:


import cProfile

def custom_profile_decorator(func):
    def wrapper(*args, **kwargs):
        profiler = cProfile.Profile()
        result = profiler.runcall(func, *args, **kwargs)
        profiler.print_stats()
        return result
    return wrapper

@custom_profile_decorator
def custom_function():
    print("That's a test")
    
custom_function():

In this example, my_function is decorated with profile_decorator. When we call my_function(), it will run the profiler and print out a report of how long each function call took.

Output:

3 function calls in 0.000 seconds

Ordered by: standard name

ncalls  tottime  percall  cumtime  percall filename:lineno(function)
      1    0.000    0.000    0.000    0.000 test1.py:24(my_function)
      1    0.000    0.000    0.000    0.000 {built-in method builtins.print}
      1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

Logging with Decorators

Logging is another useful application for decorators. It allows us to keep track of what our program does without having to manually add logging statements throughout our code. Here’s an example:


import logging

logging.basicConfig(level=logging.INFO)

def custom_log_decorator(func):
    def wrapper(*args, **kwargs):
        logging.info(f"Calling the function: {func.__name__}")
        result = func(*args, **kwargs)
        logging.info(f"The function {func.__name__} returned: {result}")
        return result
    return wrapper

@custom_log_decorator
def custom_function():
    print("That's a simple test")
    
custom_function()

In this example, custom_function is decorated with custom_log_decorator. When we call custom_function(), it will log a message before and after the function call, including any arguments passed to the function.

Output:

INFO:root:Calling the function: custom_function
That's a simple test
INFO:root:The function custom_function returned: None

Conclusion

Decorators are a powerful tool in Python that allow us to easily add functionality to existing functions without modifying them directly. They can be used for profiling, logging, and many other purposes. By understanding how decorators work, you can write cleaner, more maintainable code.

To top