Understanding Python Closures and Function Decorators
Building reusable code often leads to situations where existing functions need extra behavior without altering their original implementation. Python supports this through closures and decorators.
How Closures Work
A closure occurs when a nested function references a variable from its enclosing scope and the enclosing function returns the nested functon itself.
Consider this structure:
def outer(prefix):
def inner(message):
print(f"{prefix}: {message}")
return inner
log = outer("INFO")
log("Service started")
The inner function captures prefix from outer. Calling outer("INFO") returns inner, which is assigned to log. Invoking log later still has access to the captured prefix, producing "INFO: Service started". This delayed execution is the core of closures.
Writing Decorators
A decorator is a callable that wraps another function to extend its behavior. Its essentially a closure where the outer function receives a callable instead of a simple value.
Define a decorator that measures execution time:
import time
def timer(original_func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = original_func(*args, **kwargs)
end = time.perf_counter()
print(f"{original_func.__name__} took {end - start:.4f}s")
return result
return wrapper
Decorate a function with @timer:
@timer
def compute_squares(limit):
return [i * i for i in range(limit)]
values = compute_squares(10000)
# Output: compute_squares took 0.0012s
The @timer syntax is equivalent to compute_squares = timer(compute_squares). The wrapper runs extra logic before and after the original call, keeping the decorated function's API unchanged.
Stacking Multiple Decorators
You can apply several decorators to the same function. Execution order follows how the decorators are stacked:
def bold(fn):
def wrapper(*args, **kwargs):
return f"<b>{fn(*args, **kwargs)}</b>"
return wrapper
def italic(fn):
def wrapper(*args, **kwargs):
return f"<i>{fn(*args, **kwargs)}</i>"
return wrapper
@bold
@italic
def greet(name):
return f"Hello, {name}"
print(greet("Alice")) # <b><i>Hello, Alice</i></b>
Here italic is applied first, then bold wraps the result, producing combined markup.
Practical Use Cases
- Access control: validate user permissions before executing a view function.
- Logging: automatically record function calls, arguments, and return values.
- Caching: store results of expensive computations to avoid repeated work.
Decorators leverage closures to inject cross-cutting concerns cleanly and readably.