23.4 Stacking Multiple Decorators: Order of Application
When multiple decorators are applied to a single function, they are not executed simultaneously but rather in a specific, nested order. This process is often visualized as building an onion, where each decorator adds a new layer around the original function. The order of application is crucial because it directly dictates the runtime behavior of the decorated function. Decorators are applied from the bottom up, meaning the decorator closest to the def keyword is applied first, and the one farthest away is applied last. However, when the decorated function is called, the execution of these layers happens in the reverse order: from the outermost layer inward to the core function, and then back outward.
The Mechanics of Application Order
The syntax for stacking decorators is straightforward—they are stacked vertically above the function definition. The key to understanding the execution flow is to recognize that @decorator is syntactic sugar. The construct:
@decorator_c
@decorator_b
@decorator_a
def my_function():
...
is equivalent to the following series of assignments:
def my_function():
...
my_function = decorator_c(decorator_b(decorator_a(my_function)))
This code reveals the true nature of the operation. The innermost decorator, decorator_a, is called first, receiving the original function object. It returns a new function (or object). This return value is then passed as the argument to decorator_b, which returns another new function. Finally, that function is passed to decorator_c, and its return value is assigned to the name my_function. This bottom-to-top application creates a chain of nested function calls.
Visualizing the Execution Flow
To see this order in action, consider decorators that simply print their execution. The following example clearly demonstrates the application and runtime call order.
def decorator_a(func):
print("Applying decorator_a")
def wrapper():
print("Starting decorator_a's wrapper")
func()
print("Ending decorator_a's wrapper")
return wrapper
def decorator_b(func):
print("Applying decorator_b")
def wrapper():
print("Starting decorator_b's wrapper")
func()
print("Ending decorator_b's wrapper")
return wrapper
def decorator_c(func):
print("Applying decorator_c")
def wrapper():
print("Starting decorator_c's wrapper")
func()
print("Ending decorator_c's wrapper")
return wrapper
@decorator_c
@decorator_b
@decorator_a
def simple_function():
print("Core function executed")
print("--- Now calling the decorated function ---")
simple_function()
The output of this code is definitive:
Applying decorator_a
Applying decorator_b
Applying decorator_c
--- Now calling the decorated function ---
Starting decorator_c's wrapper
Starting decorator_b's wrapper
Starting decorator_a's wrapper
Core function executed
Ending decorator_a's wrapper
Ending decorator_b's wrapper
Ending decorator_c's wrapper
The print statements show the application order (A, then B, then C) during the decoration phase when the module is first imported. Later, when the function is called, the execution order of the wrapper functions is C -> B -> A -> core function -> A -> B -> C. The outermost decorator (decorator_c) gets the first and last word during the call.
Implications for Real-World Decorators
This order has significant practical implications. The decorator applied last is the one that runs first and last during execution. This makes it ideal for concerns that must encompass all others, such as global exception handling, logging the total execution time of the entire operation, or setting up and tearing down broad resource contexts (e.g., a database transaction).
Conversely, decorators applied first (closest to the function) run closest to the core logic. These are suited for lower-level concerns like argument validation, mutation, or access control that should happen before any other business logic.
import time
import functools
def log_execution(func):
"""Outermost: Logs the entire call. Applied last."""
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"LOG: Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"LOG: {func.__name__} returned")
return result
return wrapper
def validate_positive(func):
"""Innermost: Validates arguments. Applied first."""
@functools.wraps(func)
def wrapper(x):
if x <= 0:
raise ValueError("Input must be positive")
return func(x)
return wrapper
@log_execution # Applied second (becomes outer layer)
@validate_positive # Applied first (becomes inner layer)
def calculate_square_root(x):
return x ** 0.5
# Equivalent to: calculate_square_root = log_execution(validate_positive(calculate_square_root))
print(calculate_square_root(4))
# LOG: Calling calculate_square_root
# 2.0
# LOG: calculate_square_root returned
print(calculate_square_root(-1))
# LOG: Calling calculate_square_root
# ValueError: Input must be positive
In this example, the validation (validate_positive) happens inside the logging (log_execution). This is the correct order; we want the log to record the call even if it fails validation. If the decorators were stacked in the reverse order, the validation error would occur before the log decorator’s wrapper could run, and the call would not be logged.
Common Pitfalls and Best Practices
A primary pitfall is stacking decorators in the wrong order, leading to unintended behavior. For instance, placing a caching decorator (@functools.lru_cache) inside a decorator that generates a new random value on each call would completely negate the cache’s purpose, as the inner function would never be called twice with the same arguments from the perspective of the outer cache.
Another critical best practice is to always use functools.wraps in your decorator implementations. When decorators are stacked, the metadata of the original function (name, docstring, etc.) can be lost through multiple layers of wrapping. The @functools.wraps decorator preserves this information, which is essential for debugging and introspection tools. Without it, a stack of decorators can make it impossible to identify the original function.
Finally, be mindful of the complexity that deep stacks can introduce. While powerful, a function with five or six decorators can be difficult to reason about and debug, as the call stack becomes deeply nested. Always ensure each decorator has a single, clear responsibility and that their combined order is logical and well-documented.