Compare Python Generators, Decorators, and Context Managers
Hudson River TradingSite Reliability Engineer · Technical ScreenLast updated
Tests understanding of Python's execution timing, protocol methods, and resource cleanup across generators, decorators, and context managers, culminating in a robust lazy stream processor.
Overview
In production reliability engineering, processing large log files or stream dumps without exhausting memory or leaving file descriptors dangling is critical. Python provides three distinct abstractions that control execution flow and object lifecycles:
- Generators: Protocol defined by
__iter__()and__next__()(and lifecycle methodsclose()/throw()), allowing lazy evaluation and cooperative multitasking. - Context Managers: Protocol defined by
__enter__()and__exit__(), guaranteeing setup and deterministic teardown around a block of code. - Decorators: Higher-order callables wrapping functions or classes to extend or monitor behavior without altering core logic.
The Task
You are tasked with building a robust, audited lazy file reader that combines all three concepts.
Implement the following components:
-
track_stream(metrics: dict): A decorator for generator functions. It must track the streaming lifecycle without converting the generator into a list:- Increment
metrics['active_streams']when the generator is instantiated. - Decrement
metrics['active_streams']when the generator terminates (completed iteration, consumer early exit/break, or exception). - Increment
metrics['total_yields']for every item yielded. - If the consumer or generator encounters an unhandled exception (excluding
GeneratorExit), incrementmetrics['exceptions'].
- Increment
-
stream_chunks(file_path: str, chunk_size: int = 1024): A generator function decorated with@track_stream. It opens the specified file and yields chunks of up tochunk_sizecharacters at a time.- It must guarantee that the file descriptor is closed immediately upon termination—including when the caller breaks early, encounters an error, or the generator is garbage-collected.
Signatures
from typing import Callable, Generator, Any
def track_stream(metrics: dict) -> Callable:
...
def stream_chunks(file_path: str, chunk_size: int = 1024) -> Generator[str, None, None]:
...
Example
metrics = {'active_streams': 0, 'total_yields': 0, 'exceptions': 0}
# Assuming 'access.log' has 2500 characters:
g = stream_chunks('access.log', chunk_size=1000)
assert metrics['active_streams'] == 1
first_chunk = next(g)
assert metrics['total_yields'] == 1
assert len(first_chunk) == 1000
# Early exit
g.close()
assert metrics['active_streams'] == 0
Constraints & Edge Cases
- The file must not be read entirely into memory at once.
- Handle consumer-side
breakin aforloop cleanly (GeneratorExitshould not be counted as an exception inmetrics['exceptions']). - File handles must be closed deterministically even if an error is raised mid-consumption.
- Multiple consumers should be able to run concurrently with accurate metric counts.
Hints
Hint 1
When a caller exits a generator loop early, Python triggers generator cleanup by calling `.close()`, which raises `GeneratorExit` inside the generator at the yield point.
Hint 2
A decorator wrapping a generator function must itself return a generator object or generator-iterator to preserve lazy evaluation rather than consuming it upfront.
Hint 3
Use a `try...finally` block inside the wrapper generator to decrement active streams and finalize state regardless of whether iteration ends normally, breaks, or throws.
Follow-up questions
- What happens to the file descriptor if the consumer breaks out of a loop, but the generator object is stored in a reference cycle?
- How does `contextlib.contextmanager` use Python generators to bridge the generator protocol with `__enter__` and `__exit__` under the hood?
- How would you adapt this decorator to support asynchronous generators (`async def` with `__aiter__` and `__anext__`)?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.