Explain decorators with arguments.
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.
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.
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_executionfunction is the 'decorator factory'. It takeslog_levelas an argument. - It then returns
actual_decorator, which is the actual decorator that expects a function (func) to be decorated. - Inside
actual_decorator,wrapperis defined. This is the function that will replace the originalfuncwhen it's called. It captures thelog_levelfrom its enclosing scope (thelog_executioncall). - When
@log_execution(log_level='DEBUG')is used,log_execution('DEBUG')is called first. This returns theactual_decorator. Then,actual_decoratoris called withcalculate_sumas its argument, which in turn returns thewrapperfunction forcalculate_sum. - Similarly,
divide_numbersuses a 'WARNING' level, andgreetuses 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
wrapperfunction (andactual_decorator) can 'remember' the arguments passed to the outerdecorator_factorydue 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.