Python Decorators: Core Mechanics and Interview Patterns
A Python decorator is a higher-order function that accepts a callable and returns a replacement callable, enabling the injection of pre- or post-processing logic without altering the original function body. Applications include logging, caching, access control, performance timing, and transaction management. Extracting such cross-cutting concerns into decorators significantly improves code reusability.
Nested Functions
A nested function is defined inside another function rather than merely referencing it. The inner function resolves names by searching its local scope and then enclosing scopes.
def create_adder(base):
def add(offset):
return base + offset
return add
increment = create_adder(10)
print(increment(5)) # 15
Because add closes over base, the parameter from the outer scope remains accessible after create_adder returns. Replacing the outer variable with a callable and the inner variable with arguments yields a universal decorator pattern:
def enhanced(func):
def proxy(*args, **kwargs):
# cross-cutting logic
return func(*args, **kwargs)
return proxy
Preserving Function Metadata
A naive decorator replaces the original function with its inner wrapper, changing attributes such as __name__. To avoid this, use functools.wraps:
from functools import wraps
def trace(func):
@wraps(func)
def proxy(*args, **kwargs):
print(f"Executing {func.__name__}")
return func(*args, **kwargs)
return proxy
@trace
def greet():
print("Hello")
Calling greet() prints the execution message and retains the original name.
Parameterized Decoratros
When the decorator itself requires configuration, a third nesting level accepts the decorator arguments and returns the actual decorator:
from functools import wraps
def measure(unit):
def decorator(func):
@wraps(func)
def proxy(*args, **kwargs):
import time
start = time.time()
result = func(*args, **kwargs)
elapsed = time.time() - start
print(f"{func.__name__} finished in {elapsed:.4f} {unit}")
return result
return proxy
return decorator
@measure(unit="seconds")
def compute():
sum(range(10000))
Stacking Decorators
Multiple decorators are applied bottom-up. Consider:
def border(func):
def outer_wrap(*args, **kwargs):
print("border-start")
func(*args, **kwargs)
print("border-end")
return outer_wrap
def label(func):
def inner_wrap(*args, **kwargs):
print(f"target: {func.__name__}")
func(*args, **kwargs)
return inner_wrap
@border
@label
def action():
print("running")
action()
This is equivalent to action = border(label(action)). Execution proceeds from the outermost wrapper inward:
border-start
target: action
running
border-end
The innermost decorator executes its side effects closest to the original callable, while the outermost controls the first and last actions of the combined call.