🐍 Python Q65 / 183

Explain decorators with arguments.

AI-Powered Answer ✓ Answered

Decorators in Python provide a way to modify or enhance functions or methods. Decorators with arguments take this concept a step further, allowing you to pass configuration parameters to the decorator itself, making them more flexible and reusable for different contexts. This is achieved through an additional layer of function nesting.

Understanding the Structure

When a decorator needs arguments, it means the decorator itself is returned by another function, often called the 'decorator factory'. This factory function accepts the arguments for the decorator and then returns the actual decorator function. The actual decorator then takes the function to be decorated as its argument and returns the wrapper function, which finally executes the original function along with any added logic.

python
def decorator_factory(arg1, arg2):
    def actual_decorator(func):
        def wrapper(*args, **kwargs):
            # Logic using arg1, arg2, func, args, kwargs
            print(f"Decorator arguments: {arg1}, {arg2}")
            print(f"Calling function: {func.__name__}")
            result = func(*args, **kwargs)
            print("Function call finished")
            return result
        return wrapper
    return actual_decorator

@decorator_factory("value1", 123)
def my_function(a, b):
    print(f"Inside my_function with {a} and {b}")
    return a + b

# The decoration happens at definition time
# Calling my_function actually calls the wrapper returned by actual_decorator
result = my_function(10, 20)
print(f"Result: {result}")

Example: Logger Decorator with Levels

Let's create a logger decorator that can take a log level (e.g., 'INFO', 'WARNING', 'ERROR') as an argument. This allows us to reuse the same logging logic but customize the severity level for different functions.

python
import datetime

def log_execution(log_level='INFO'):
    """
    A decorator factory that returns a decorator to log function execution.
    The log_level argument determines the severity of the log message.
    """
    def actual_decorator(func):
        def wrapper(*args, **kwargs):
            timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
            log_message = f"[{timestamp}] [{log_level}] Calling function '{func.__name__}' " \
                          f"with args: {args}, kwargs: {kwargs}"
            print(log_message)

            try:
                result = func(*args, **kwargs)
                print(f"[{timestamp}] [{log_level}] Function '{func.__name__}' completed. Result: {result}")
                return result
            except Exception as e:
                print(f"[{timestamp}] [ERROR] Function '{func.__name__}' failed with exception: {e}")
                raise
        return wrapper
    return actual_decorator

@log_execution(log_level='DEBUG')
def calculate_sum(a, b):
    """Calculates the sum of two numbers."""
    return a + b

@log_execution(log_level='WARNING')
def divide_numbers(numerator, denominator):
    """Divides two numbers, handles division by zero."""
    if denominator == 0:
        raise ValueError("Cannot divide by zero!")
    return numerator / denominator

@log_execution()
def greet(name):
    """Greets a person."""
    return f"Hello, {name}!"

# --- Demonstrating Usage ---
print("\n--- Sum Calculation ---")
sum_result = calculate_sum(10, 5)
print(f"Result: {sum_result}")

print("\n--- Division ---")
try:
    div_result = divide_numbers(20, 4)
    print(f"Result: {div_result}")
    div_result_error = divide_numbers(10, 0)
    print(f"Result: {div_result_error}") # This line won't be reached
except ValueError as e:
    print(f"Caught error: {e}")

print("\n--- Greeting ---")
greeting_msg = greet("Alice")
print(f"Result: {greeting_msg}")

In this example:

  • The log_execution function is the 'decorator factory'. It takes log_level as an argument.
  • It then returns actual_decorator, which is the actual decorator that expects a function (func) to be decorated.
  • Inside actual_decorator, wrapper is defined. This is the function that will replace the original func when it's called. It captures the log_level from its enclosing scope (the log_execution call).
  • When @log_execution(log_level='DEBUG') is used, log_execution('DEBUG') is called first. This returns the actual_decorator. Then, actual_decorator is called with calculate_sum as its argument, which in turn returns the wrapper function for calculate_sum.
  • Similarly, divide_numbers uses a 'WARNING' level, and greet uses the default 'INFO' level.

Key Points

  • Three Layers: Decorators with arguments typically involve three nested functions: the decorator factory (takes decorator arguments), the actual decorator (takes the function to decorate), and the wrapper function (replaces the decorated function and executes the logic).
  • Execution Flow: The decorator factory is executed when the decorated function is defined (at import time or when the code block is parsed), not when the decorated function is called.
  • State Preservation: The inner wrapper function (and actual_decorator) can 'remember' the arguments passed to the outer decorator_factory due to closures.
  • Flexibility: This pattern allows for highly configurable and reusable decorators, making your code cleaner and more modular for tasks like logging, authentication, caching, or performance monitoring.