Python FAQ: Top Questions
20. What are Python decorators with arguments, and how do they differ from simple decorators?
As discussed in Q10, decorators are functions that wrap other functions to extend or modify their behavior. A **simple decorator** (like `@timer_decorator`) is a function that takes a function as input and returns a new function. **Decorators with arguments** take this concept a step further, allowing you to pass configuration parameters to the decorator itself when you apply it.
How Simple Decorators Work (Review):
A simple decorator (without arguments) looks like this:
def simple_decorator(func):
def wrapper(*args, **kwargs):
# ... do something before func ...
result = func(*args, **kwargs)
# ... do something after func ...
return result
return wrapper
@simple_decorator
def my_function():
pass
# This is equivalent to: my_function = simple_decorator(my_function)
Here, `simple_decorator` is directly called with `my_function` as its argument when Python encounters the `@` syntax.
Decorators with Arguments:
When you need to pass arguments to a decorator (e.g., `@log_level('INFO')`), the decorator needs to be structured differently. It effectively becomes a **decorator factory** – a function that returns a decorator.
The syntax for a decorator with arguments is:
@decorator_factory(arg1, arg2, ...)
def my_function():
pass
This is equivalent to:
my_function = decorator_factory(arg1, arg2, ...)(my_function)
Mechanism of Decorators with Arguments:
To implement a decorator that accepts arguments, you need an additional layer of nesting:
- **Outer Function (Decorator Factory):** This function takes the arguments for the decorator itself (e.g., `log_level`, `permission_type`). It doesn't take the function to be decorated as an argument yet. It returns the actual decorator function.
- **Middle Function (The Actual Decorator):** This is the function that takes the function to be decorated (`func`) as an argument. This is the part that will have the `@functools.wraps` annotation. It returns the `wrapper` function.
- **Inner Function (The Wrapper):** This is the innermost function that actually replaces the decorated function. It takes `*args` and `**kwargs` of the original function and contains the code that runs before and after calling the original function.
This three-layer structure allows the outer function to process the decorator's arguments *first*, then return the inner decorator (which "remembers" these arguments via closure), and *that* inner decorator then processes the function to be decorated.
Benefits:
- Configuration: Allows you to customize the behavior of the decorator at the point of application.
- Flexibility: Makes decorators more versatile for a wider range of use cases.
- DRY Principle: Further promotes code reusability by having a single decorator implementation that can be configured differently.
Decorators with arguments are extremely common in web frameworks (e.g., `@route('/path', methods=['GET'])`) and authentication systems, where the decorator needs specific parameters to perform its logic.
import functools
import time
# --- Example 1: Simple Decorator (Review from Q10) ---
print("--- Simple Decorator ---")
def simple_timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"'{func.__name__}' ran in {end - start:.4f} seconds.")
return result
return wrapper
@simple_timer
def greet_user(name):
time.sleep(0.1)
print(f"Hello, {name}!")
greet_user("Alice")
greet_user("Bob")
# --- Example 2: Decorator with Arguments (Logging Level) ---
print("\n--- Decorator with Arguments (Logging Level) ---")
def log_with_level(level): # This is the outer function (decorator factory)
def decorator(func): # This is the actual decorator function
@functools.wraps(func)
def wrapper(*args, **kwargs): # This is the wrapper function
if level == 'DEBUG':
print(f"DEBUG: Entering '{func.__name__}' with args={args}, kwargs={kwargs}")
elif level == 'INFO':
print(f"INFO: Executing '{func.__name__}'...")
elif level == 'WARNING':
print(f"WARNING: '{func.__name__}' might be a critical call.")
result = func(*args, **kwargs)
if level == 'DEBUG':
print(f"DEBUG: Exiting '{func.__name__}' with result={result}")
return result
return wrapper
return decorator # The decorator factory returns the actual decorator
@log_with_level('INFO') # Here, 'INFO' is passed to log_with_level
def process_data(data_list):
"""Processes a list of data."""
time.sleep(0.05)
return sum(data_list)
@log_with_level('DEBUG') # Here, 'DEBUG' is passed
def calculate_average(numbers):
"""Calculates the average of numbers."""
if not numbers:
return 0
return sum(numbers) / len(numbers)
@log_with_level('WARNING') # Here, 'WARNING' is passed
def send_alert(message):
"""Sends an alert message."""
print(f"ALERT SENT: {message}")
return True
print("\n--- Calling decorated functions with logging levels ---")
process_data([1, 2, 3])
calculate_average([10, 20, 30])
send_alert("System offline!")
# --- Example 3: Decorator with Arguments (Permission Check) ---
print("\n--- Decorator with Arguments (Permission Check) ---")
user_roles = {"admin": ["admin", "editor"], "guest": ["viewer"], "editor": ["editor"]}
def require_role(required_role):
def decorator_for_role(func):
@functools.wraps(func)
def wrapper(user_type, *args, **kwargs):
if user_type in user_roles.get(required_role, []):
print(f"Access granted for '{user_type}' to '{func.__name__}'.")
return func(user_type, *args, **kwargs)
else:
print(f"Access DENIED for '{user_type}' to '{func.__name__}'. Required role: '{required_role}'.")
return None # Or raise an exception
return wrapper
return decorator_for_role
@require_role("admin")
def delete_critical_data(user):
print(f"{user} deleted critical data.")
return True
@require_role("editor")
def publish_article(user, article_id):
print(f"{user} published article {article_id}.")
return True
@require_role("viewer")
def view_report(user, report_name):
print(f"{user} viewed report '{report_name}'.")
return True
print("\n--- Testing permission checks ---")
delete_critical_data("admin")
delete_critical_data("guest")
delete_critical_data("editor")
print("-" * 20)
publish_article("editor", 123)
publish_article("guest", 456)
print("-" * 20)
view_report("viewer", "Sales_Q1")
view_report("admin", "User_Activity") # Admin also has viewer role implicitly via user_roles dict
Explanation of the Example Code:
-
**Simple Decorator (`simple_timer`):**
- This is a standard decorator from Q10. It takes the function `greet_user` directly, wraps it, and returns the `wrapper` function. The `@simple_timer` syntax implicitly passes `greet_user` to `simple_timer`.
-
**Decorator with Arguments (`log_with_level`):**
- **Outer function (`log_with_level(level)`):** This is the "decorator factory." It takes the `level` argument ('INFO', 'DEBUG', 'WARNING'). It then *returns* the `decorator` function.
- **Middle function (`decorator(func)`):** This is the actual decorator. It takes the function to be decorated (`process_data`, `calculate_average`, `send_alert`) as its argument. It then returns the `wrapper` function.
- **Inner function (`wrapper(*args, **kwargs)`):** This is the function that will ultimately replace the decorated function. It uses the `level` argument (which is captured from the outer scope via a closure) to determine what type of log message to print.
- When you write `@log_with_level('INFO')`, Python first calls `log_with_level('INFO')`, which returns the `decorator` function. Then, this returned `decorator` function is applied to `process_data`, equivalent to `process_data = decorator(process_data)`. This three-layer structure allows the decorator to receive and use configuration arguments.
-
**Decorator with Arguments (`require_role`):**
- This is another powerful example of a decorator with arguments, used for authentication/authorization.
- `require_role(required_role)` takes the role needed for access (e.g., "admin", "editor").
- The `wrapper` function checks if the `user_type` (passed as the first argument to the decorated function) is present in the `user_roles` mapping for the `required_role`.
- If the role matches, the original function is called; otherwise, an access denied message is printed, and `None` is returned.
- This clearly shows how the decorator's arguments (`required_role`) are used to customize its behavior (the permission check logic) for different functions.
These examples demonstrate that decorators with arguments require an extra layer of function nesting, creating a "decorator factory" that first processes the decorator's arguments, and then returns the actual decorator that wraps the target function. This pattern provides immense flexibility and reusability for common functional enhancements.