In Python, we can have asynchronous iterators. If you are familiar with iterators, you know that they are required to implement __iter__()
and __next__()
methods. Similarly, asynchronous iterators are required to implement __aiter__()
and __anext__()
methods. The __anext__()
method must return an awaitable object, usually the current iterator itself.
Asynchronous generators are the most basic forms of asynchronous iterators as the __aiter__()
and __anext__()
methods are implemented automatically. An asynchronous generator is simply a coroutine with one or more yield
statements.
The get_evens()
function above is an example of a simple asynchronous generator. It takes an integer n
as argument, then yields all the even numbers from 0
to n
.
The builtin anext()
function retrieves the next object in an asynchronous iterator it is analogous to the next()
function in regular iterators.
Note that unlike in regular generators where calling the next()
function on it will return the actual yielded value, with the anext()
function, we have to await the returned object in order to retrieve the actual value.
basic "async for" usage
Consider if we wanted to repeatedly iterate through the elements of the previous asynchronous generator(get_evens()
) in a loop so that we do not need to await the values manually. We cannot do this with regular loops because for one reason, asynchronous generators are not iterable.
As shown above, a TypeError
exception gets raised because the asynchronous generators are considered not iterable, this is why we need async for
expressions. The above example will work as intended if we use async for
instead of regular for
loops.
As shown above, the yielded values are correctly retrieved when we use an asynchronous generator with an async for expression. Note that the async for
expression automatically awaits the yielded awaitables. It also must be used inside of a coroutine function, we cannot use them as standalone statements as this will lead to a SyntaxError
exception being raised, this is shown below.
async for vs regular for loops
In order to better understand how the async for
expressions works, let us compare them with their counterparts, the for
loops.
Simply put, for
loops works with regular iterators and iterables while async for
will only work with asynchronous iterators. Asynchronous iterators are not necessarily iterables and they cannot be used in regular for loops.
The name "async for"
may be misunderstood to mean that it is a parallelized for
loop and that the elements of the target asynchronous iterator are being traversed asynchronously i.e in parallel, but as we have already seen, this is not so. The async for
expression simply iterates sequentially through the items of an async source.
Let us understand this better with an example that is closely related to our previous example:
The get_odds()
above is an asynchronous generator that yields odd numbers from 0
to n
. We used the async for
expression inside main()
to retrieve the yielded values.
The closest way of doing the same with a regular for loop would be to create a list of the awaitables and then await each in a for loop as shown below:
As shown, with regular loops, you have to manually await the yielded awaitables but with async for
expression, they are awaited automatically.
Another common error is to assume that async for
will work on iterables such as lists that contains awaitable objects. However, this will blatantly fail.
As shown above, the async for
expression requires the target object to have the __aiter__()
method, if not so a TypeError
exception is raised.
async for with custom asynchronous iterators
So far we have been using asynchronous generators in which the __aiter__()
and __anext__()
methods are implemented automatically. With custom asynchronous iterators, the two methods have to be implemented manually in the class definition. In this section we will work with such objects.
Each of the __aiter__()
and __anext__()
methods have its own purpose. The __aiter__()
method returns an instance of the iterator, while the __anext__()
method returns the next awaitable in the sequence.
After implementing the two method, the objects of the class become eligible for use in async for
expressions.
Let us create the equivalent iterator to the get_evens()
generator in our previous examples. The iterator will generate even numbers starting from 0
up to n
.
Calling EvensGenerator(n)
creates an EvensGenerator
object. Since the Evensgenerator
class implements __aiter__()
and __anext__()
, we can use the returned object in an async for
expression.
In our example above, the __aiter__()
method simply returns the current object i.e self
. The __anext__()
method implements the logic for moving from one even number to the next until n
is reached, in which case the StopAsycIteration
exception is raised. The StopAsyncIteration
is analogous to the StopIteration
exception in regular iterators.
Note that unlike __aiter__()
, the __anext__()
method is defined as a coroutine (with async def
) as it is supposed to return an awaitable object. Failure to define __anext__()
as a coroutine will raise a TypeError
exception, as demonstrated below.
The TypeError
exception is raised because the __anext__()
method is supposed to return an awaitable object but in the above case, it returned an integer.