[Python-ideas] async/await in Python

Piotr Jurkiewicz piotr.jerzy.jurkiewicz at gmail.com
Sun Apr 19 05:19:21 CEST 2015


1. Overall I like the proposal very much. However, I have got one 
semantic remark. You propose `async for` as a syntax for asynchronous 
iterators:

     async for row in Cursor():
         print(row)

Wouldn't it be more semantically correct to use `await for` instead of 
`async for`?

     await for row in Cursor():
         print(row)

For me the word 'await' is an indicator that I am awaiting for some 
value being returned. For example, with simple `await` expression I am 
awaiting for a data being fetched from db:

     data = await db.fetch('SELECT ...')

When I use asynchronous iterator I am awaiting for a value being 
returned as well. For example I am awaiting (in each iteration) for a 
row from a cursor. Therefore, it seems to me to be natural to use word 
'await' instead of 'async'. Furthermore syntax 'await for row in cursor' 
reassembles natural English language.

On the other hand, when I use context manager, I am not awaiting for any 
value, so syntax `async with` seems to be proper in that case:

     async with session.transaction():
             ...
             await session.update(data)

Dart, for example, goes that way. They use `await` expression for 
awaiting single Future and `await for` statement for asynchronous iterators:

     await for (variable declaration in expression) {
       // Executes each time the stream emits a value.
     }

2. I would like to go little beyond this proposal and think about 
composition of async coroutines (aka waiting for multiple coroutines). 
For example C# has helper functions WhenAll and WhenAny for that:

    await Task.WhenAll(tasks_list);
    await Task.WhenAny(tasks_list);

In asyncio module there is a function asyncio.wait() which can be used 
to achieve similar result:

     asyncio.wait(fs, timeout=None, return_when=ALL_COMPLETED)
     asyncio.wait(fs, timeout=None, return_when=FIRST_COMPLETED)

However, after introduction of `await` its name becomes problematic. 
First, it reassembles `await` too much and can cause a confusion. 
Second, its usage would result in an awkward 'await wait':

     done, pending = await asyncio.wait(coroutines_list)
     results = []
     for task in done:
         results.append(task.result())

Another problem with asyncio.wait() is that it returns Tasks, not their 
results directly, so user has to unpack them. There is function 
asyncio.gather(*coros_or_futures) which return results list directly, 
however it can be only used for ALL_COMPLETED case. There is also a 
function asyncio.wait_for() which (unlike asyncio.wait()) unpacks the 
result, but can only be used for one coroutine (so what is the 
difference from `await` expression?). Finally, there is 
asyncio.as_completed() which returns iterator for iterating over 
coroutines results as they complete (but I don't know how exactly this 
iterator relates to async iterators proposed here).

I can imagine the set of three functions being exposed to user to 
control waiting for multiple coroutines:

asynctools.as_done()  # returns asynchronous iterator for iterating over 
the results of coroutines as they complete
asynctools.all_done() # returns a future aggregating results from the 
given coroutine objects, which awaited returns list of results (like 
asyncio.gather())
asynctools.any_done() # returns a future, which awaited returns result 
of first completed coroutine

Example:

     from asynctools import as_done, all_done, any_done

     corobj0 = async_sql_query("SELECT...")
     corobj1 = async_memcached_get("someid")
     corobj2 = async_http_get("http://python.org")

     # ------------------------------------------------

     # Iterate over results as coroutines complete
     # using async iterator

     await for result in as_done([corobj0, corobj1, corobj2]):
         print(result)

     # ------------------------------------------------

     # Await for results of all coroutines
     # using async iterator

     results = []
     await for result in as_done([corobj0, corobj1, corobj2]):
         results.append(result)

     # or using shorthand coroutine all_done()

     results = await all_done([corobj0, corobj1, corobj2])

     # ------------------------------------------------

     # Await for a result of first completed coroutine
     # using async iterator

     await for result in as_done([corobj0, corobj1, corobj2]):
         first_result = result
         break

     # or using shorthand coroutine any_done()

     first_result = await any_done([corobj0, corobj1, corobj2])

I deliberately placed these functions in a new asynctools module, not in 
the asyncio module. I find asyncio module being too much complicated to 
expose it to an ordinary user. There are four very similar concepts used 
in it: Coroutine (function), Coroutine (object), Future and Task. In 
addition many functions accept both coroutines and Futures in the same 
argument, Task is a subclass of Future -- it makes people very confused. 
It is difficult to grasp what are differences between them and how they 
relate to each other. For comparison in JavaScript that are only two 
concepts: async functions and Promises.

(Furthermore, after this PEP being accepted there will be fifth concept: 
old-style coroutines. And there are also concurrent.futures.Futures...)

Personally, I think that asyncio module should be refactored and broken 
into two separate modules, named for example:

- asyncloop  # consisting low-level loop-related things, mostly not 
intended to be used by the average user (apart from get_event_loop() and 
run_until_xxx())
- asynctools # consisting high-level helper functions, like described before

As with this PEP async/await will become first class member of Python 
environment, all rest high-level functions should be in my opinion moved 
from asyncio to appropriate modules, like socket or subprocess. These 
are the places where users will be looking for them. For example:

     socket.socket.recv()
     socket.socket.recv_async()
     socket.socket.sendall()
     socket.socket.sendall_async()
     socket.getaddrinfo()
     socket.getaddrinfo_async()

Finally, concurrent.futures should either be renamed to avoid the usage 
of word 'future', or be made compatible with async/await.

I know that I went far beyond scope of this PEP, but I think that these 
are the issues which will pop up after acceptance of this PEP sooner or 
later.

Finally, I remind about my proposal from the beginning of this email, to 
use `await for` instead of `async for` for asynchronous iterators. 
What's your opinion about that?

Piotr


More information about the Python-ideas mailing list