Decorators & Closures
Overview
Decorators let you modify or extend a function's behavior dynamically without modifying its source code.
Learning Objectives
- Create basic decorators.
- Preserve original function metadata using wraps.
- Construct closures that capture outer scopes.
Concept Explanation
A decorator takes a function as an argument, wraps it with custom logic, and returns the wrapper function. Closures allow inner functions to reference variables in enclosing scopes even after the outer function has finished executing.
Code Examples
Example 1 — Basics
This example introduces the fundamental syntax and concepts.
def announce(func):
def wrapper():
print('Function starting')
return func()
return wrapper
Example 2 — Everyday Usage
This example demonstrates a realistic scenario handling business parameters.
from functools import wraps
def log_call(func):
@wraps(func) # Preserves name and metadata
def wrapper(*args, **kwargs):
print(f'Running: {func.__name__}')
return func(*args, **kwargs)
return wrapper
@log_call
def add(a, b): return a + b
print(add(5, 7))
Example 3 — Advanced Example
This example shows clean, production-grade code structure following senior development standards.
# Timer decorator measuring function execution time
import time
from functools import wraps
def measure_time(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
duration = time.perf_counter() - start
print(f'{func.__name__} took {duration:.4f} seconds')
return result
return wrapper
@measure_time
def compute_squares():
return [x * x for x in range(10000)]
compute_squares()
Visual Flow
The following execution flow represents the step-by-step evaluation inside the interpreter:
Call decorated function → Execute wrapper pre-logic → Run original function → Execute wrapper post-logic → Return output
Common Mistakes
Review these common pitfalls when working with this topic:
- Forgetting to return the inner wrapper function from the decorator.
- Not using @wraps, which overwrites the decorated function's name and metadata.
- Forgetting to pass *args and **kwargs inside the wrapper function.
- Modifying closure variables directly without nonlocal declarations.
Best Practices
Enforce these Pythonic best practices in your codebase:
def log(f):
def wrap(*args):
return f(*args)
return wrap # Missing wraps metadata preservation
from functools import wraps
def log(f):
@wraps(f)
def wrap(*args, **kw):
return f(*args, **kw)
return wrap # Metadata preserved
Quick Revision
Use these key summaries for last-minute revision:
- Decorators wrap functions to add behaviors.
- Closures capture outer variables.
- Use @functools.wraps to keep metadata.
- wrapper must accept *args and **kwargs.
- Apply decorators using the @ symbol.
- Decorators compile to func = decorator(func).