Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome back to our course on asyncio.
In the last lesson, we defined what an asynchronous generator is—an async def function containing yield—and discussed conceptually why it's a more elegant way to create asynchronous iterators. We noted its conciseness and readability compared to the more verbose class-based protocol.
Today, we'll make that comparison concrete. Our goal is to refactor a class-based asynchronous iterator into a more concise asynchronous generator function. This practical exercise will solidify your understanding of the benefits and show you the idiomatic way to produce asynchronous data streams in Python.
1. The "Before" State: Our Class-Based Iterator
Two lessons ago, we implemented a custom asynchronous iterator class, NetworkDataStream, to simulate fetching data chunks over a network. Let's revisit that code. It serves as our "before" state for the refactoring.
import asyncio
import time
class NetworkDataStream:
"""
A class-based asynchronous iterator that simulates fetching
data chunks with a delay.
"""
def __init__(self, total_chunks: int, delay: float):
self._total_chunks = total_chunks
self._delay = delay
self._current_chunk = 0
def __aiter__(self):
# The asynchronous iterator object is itself.
return self
async def __anext__(self):
# Check if we have finished iterating.
if self._current_chunk >= self._total_chunks:
raise StopAsyncIteration
# Simulate a non-blocking I/O operation.
await asyncio.sleep(self._delay)
# Prepare the data chunk.
self._current_chunk += 1
data_chunk = f"Chunk {self._current_chunk}/{self._total_chunks}"
# Return the data chunk.
return data_chunk
async def main():
print(f"Start time: {time.strftime('%X')}")
# Using the class-based iterator
stream = NetworkDataStream(total_chunks=5, delay=1.0)
async for chunk in stream:
print(f" -> Received: {chunk} at {time.strftime('%X')}")
print(f"End time: {time.strftime('%X')}")
if __name__ == "__main__":
asyncio.run(main())
Take a moment to review this implementation. Notice the boilerplate:
- State is managed explicitly through instance attributes (
_total_chunks,_delay,_current_chunk). __aiter__is required and simply returnsself.__anext__contains the core logic but also needs to handle raisingStopAsyncIterationmanually.
The logic feels somewhat fragmented across __init__ and __anext__.
2. The "After" State: Refactoring to an Async Generator
Now, let's transform this class into an asynchronous generator function. The goal is to achieve the exact same behavior with simpler, more readable code.
Here is the refactored version:
import asyncio
import time
async def network_data_stream_generator(total_chunks: int, delay: float):
"""
An asynchronous generator that achieves the same result as the
NetworkDataStream class, but far more concisely.
"""
for i in range(total_chunks):
# Simulate a non-blocking I/O operation.
await asyncio.sleep(delay)
# Yield the data chunk.
yield f"Chunk {i+1}/{total_chunks}"
async def main():
print(f"Start time: {time.strftime('%X')}")
# Using the async generator
async for chunk in network_data_stream_generator(total_chunks=5, delay=1.0):
print(f" -> Received: {chunk} at {time.strftime('%X')}")
print(f"End time: {time.strftime('%X')}")
if __name__ == "__main__":
asyncio.run(main())
Let's analyze the transformation:
- No Class Needed: The entire
NetworkDataStreamclass is replaced by a single function,network_data_stream_generator. - State is Implicit: The state that was managed by
self._current_chunkis now implicitly handled by theforloop's counter (i). The Python runtime automatically saves and restores the state of this local variable acrossawaitandyieldpoints. - No Protocol Methods: The
__aiter__and__anext__methods are gone. Theasync defandyieldkeywords signal to Python to create an object that implements this protocol for us. StopAsyncIterationis Automatic: TheStopAsyncIterationexception is raised automatically when the generator function completes (i.e., when theforloop finishes).
The result is code that is not only shorter but also expresses its intent more directly. The logic reads as a simple, sequential process.
3. A Practical Refactoring Example: API Pagination
This pattern is extremely common. Imagine fetching data from a paginated API where each page requires a separate network request. This is a perfect use case for an async iterator.
The following resource provides an excellent, direct comparison of a class-based iterator for this task versus its refactored async generator counterpart.
This article provides a fantastic side-by-side comparison of a class-based async iterator for handling paginated HTTP requests and its equivalent, much simpler, async generator function.
Read the section 'Async Generator Function'. Focus on the two code blocks showing the HttpPaginationAsyncIterator class and the simplified get_all_data async generator function. Note how the state management (self._next_url) is simplified into a local variable (next_url) within the while loop.
As you can see from the article's example, the generator version is far more intuitive. The while next_url: loop clearly describes the process, and the state is just a local variable, not an instance attribute.
4. When to Use Which: The Trade-offs
So, if async generators are so much simpler, is there ever a reason to write a class-based async iterator? The answer is "rarely," but it's important to understand the trade-offs.
The following video does an excellent job of demonstrating this. It first builds a rate-limiter using a clean async generator, and then, for the sake of comparison, implements the exact same logic using the full class-based protocol. The contrast makes the argument for generators very compelling.
This video segment first builds a rate-limiter using an async generator, and then rebuilds the exact same logic using the class-based protocol. The narrator's commentary on the complexity difference is the key takeaway.
Watch from 09:01 to 16:13. First, you'll see a clean implementation of a rate limiter using an async generator. Then, the video refactors this into a class. Pay close attention to the narrator's conclusion around 15:30, where he explains why the generator approach is usually preferred and when the class-based approach might still be necessary.
As the video explains, you should generally prefer async generators. They are more readable, more concise, and less prone to errors.
You might consider a class-based async iterator only in specific, complex scenarios:
- Complex State: If the iterator's state is highly complex and requires multiple methods to manage or maintain invariants, encapsulating it within a class can be a good design choice.
- Exposing Additional Methods: If the iterator object itself needs to expose other public methods beyond just iteration (e.g., a
reset()orseek()method), a class is necessary.
For the vast majority of use cases you'll encounter, an asynchronous generator is the superior tool for the job.
Conclusion
In this lesson, we moved from theory to practice, demonstrating the power and elegance of asynchronous generators by refactoring a class-based iterator.
- Key Takeaway: Refactoring a class-based async iterator into an async generator dramatically simplifies code. It replaces the manual implementation of the iterator protocol (
__aiter__,__anext__,StopAsyncIteration) and explicit state management (self.variable) with a single function that uses a loop andyield. - Core Concept: The primary benefit comes from the fact that the generator's execution frame automatically preserves state between
yieldpoints, making it a natural and efficient construct for building iterators. - Best Practice: Unless you have a specific need for the complex state encapsulation that a class provides, always prefer asynchronous generators for creating asynchronous iterators.
Now that we have mastered creating and consuming asynchronous data streams, our next lesson will address a related challenge: managing asynchronous resources, such as network connections or files that must be opened and closed correctly within an asyncio application. We will explore the async with statement and learn how to build our own asynchronous context managers.