[Python-ideas] async/await in Python

Yury Selivanov yselivanov.ml at gmail.com
Sun Apr 19 06:08:09 CEST 2015


Hi Piotr,

Thank you very much for your detailed feedback. Answers below:

On 2015-04-18 11:19 PM, Piotr Jurkiewicz wrote:
> 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)

I like that the current proposal is simple.  You use 'await'
keyword only to call a coroutine, and you use 'async' only as a
modifier, i.e. 'async def' becomes a coroutine, 'async for' is an
asynchronous iteration block, etc.  The less confusion we have
the better.

>
> 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.

To me it reads different. There is no value of 'for' statement in
Python, it's a block of code.  Hence we use 'async' to mark it as
an asynchronous block of code.

>
> 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.

You're exactly right--your ideas are very nice and sound,--but
are outside of the scope of PEP 492.  One step at a time.  First,
we do the syntax changes and integrate only the most important
functions and builtins.  Later, we get the feedback and integrate
other important features to the standard library (for instance,
I liked your idea about asyncloop module. I think it should be
prototyped and put on PyPI if the PEP is accepted).


Thanks a lot!

Yury


More information about the Python-ideas mailing list