Python Asynchronous Comprehensions - how do they work?

11,446

Solution 1

You are basically asking how an async for loop works over a regular loop. That you can now use such a loop in a list comprehension doesn't make any difference here; that's just an optimisation that avoids repeated list.append() calls, exactly like a normal list comprehension does.

An async for loop then, simply awaits each next step of the iteration protocol, where a regular for loop would block.

To illustrate, imagine a normal for loop:

for foo in bar:
    ...

For this loop, Python essentially does this:

bar_iter = iter(bar)
while True:
    try:
        foo = next(bar_iter)
    except StopIteration:
        break
    ...

The next(bar_iter) call is not asynchronous; it blocks.

Now replace for with async for, and what Python does changes to:

bar_iter = aiter(bar)  # aiter doesn't exist, but see below
while True:
    try:
        foo = await anext(bar_iter)  # anext doesn't exist, but see below
    except StopIteration:
        break
    ...

In the above example aiter() and anext() are fictional functions; these are functionally exact equivalents of their iter() and next() brethren but instead of __iter__ and __next__ these use __aiter__ and __anext__. That is to say, asynchronous hooks exist for the same functionality but are distinguished from their non-async variants by the prefix a.

The await keyword there is the crucial difference, so for each iteration an async for loop yields control so other coroutines can run instead.

Again, to re-iterate, all this already was added in Python 3.5 (see PEP 492), all that is new in Python 3.6 is that you can use such a loop in a list comprehension too. And in generator expressions and set and dict comprehensions, for that matter.

Last but not least, the same set of changes also made it possible to use await <expression> in the expression section of a comprehension, so:

[await func(i) for i in someiterable]

is now possible.

Solution 2

I think I understand that the aiter() function gets called asynchronously, so that each iteration of aiter can proceed without the previous one necessarily returning yet (or is this understanding wrong?).

That understanding is wrong. Iterations of an async for loop cannot be performed in parallel. async for is just as sequential as a regular for loop.

The asynchronous part of async for is that it lets the iterator await on behalf of the coroutine iterating over it. It's only for use within asynchronous coroutines, and only for use on special asynchronous iterables. Other than that, it's mostly just like a regular for loop.

Share:
11,446
Andrew Guy
Author by

Andrew Guy

Updated on June 15, 2022

Comments

  • Andrew Guy
    Andrew Guy almost 2 years

    I'm having trouble understanding the use of asynchronous comprehensions introduced in Python 3.6. As a disclaimer, I don't have a lot of experience dealing with asynchronous code in general in Python.

    The example given in the what's new for Python 3.6 document is:

    result = [i async for i in aiter() if i % 2]
    

    In the PEP, this is expanded to:

    result = []
    async for i in aiter():
        if i % 2:
            result.append(i)
    

    I think I understand that the aiter() function gets called asynchronously, so that each iteration of aiter can proceed without the previous one necessarily returning yet (or is this understanding wrong?).

    What I'm not sure about is how that then translates to the list comprehension here. Do results get placed into the list in the order that they are returned? Or are there effective 'placeholders' in the final list so that each result is placed in the list in the right order? Or am I thinking about this the wrong way?

    Additionally, is someone able to provide a real-world example that would illustrate both an applicable use case and the basic mechanics of async in comprehensions like this?