Decorators & Generators
Write reusable decorators, lazy generators, and custom iterators for expressive Python.
Function Decorators
Decorators wrap functions to add logging, caching, authentication, or timing. @decorator syntax applies wrapper at definition time.
Use functools.wraps to preserve wrapped function metadata for introspection and debugging.
Parameterized decorators are factories returning the actual decorator—three levels of nested functions.
- Keep decorator overhead minimal on hot paths
- Document decorator side effects in docstrings
- Test decorated functions behave like originals
def retry(times=3):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
for attempt in range(times):
try:
return fn(*args, **kwargs)
except TransientError:
if attempt == times - 1:
raise
return wrapper
return decoratorGenerators and yield
Generators produce values lazily with yield, pausing execution between items. They consume constant memory for large sequences versus building lists.
Generator expressions mirror list comprehensions: (x*x for x in range(10)). Use when you iterate once and do not need random access.
yield from delegates to sub-generators, flattening nested iteration cleanly.
- Close resources in generators with try/finally or context managers
- Materialize to list only when reuse is required
- Use itertools for advanced generator composition
def read_lines(path):
with open(path) as f:
for line in f:
yield line.rstrip("\n")Custom Iterators
Classes implementing __iter__ returning self and __next__ raising StopIteration are iterators. Often generators are simpler than manual iterator classes.
Infinite generators model streams: paginated API results, sensor readings. Consumers should break explicitly to avoid infinite loops.
Async generators with async for integrate with asyncio for streaming HTTP or websocket data.
- Prefer yield-based generators over manual __next__
- Document whether iterators are single-pass or reusable
- Use tee() sparingly—it buffers consumed values