[Python-ideas] async/await in Python
Chris Angelico
rosuav at gmail.com
Sat Apr 18 00:00:03 CEST 2015
On Sat, Apr 18, 2015 at 4:58 AM, Yury Selivanov <yselivanov.ml at gmail.com> wrote:
> The following new syntax is used to declare a coroutine::
>
> async def read_data(db):
> pass
>
> Key properties of coroutines:
>
> * Coroutines are always generators, even if they do not contain ``await``
> expressions.
Do you mean that a coroutine is a special form of generator, or that a
function declared with "async" is always a coroutine even if it
doesn't contain an "await"? You're separating coroutines from
generators in many ways, so maybe I'm just misreading this.
> Asynchronous Context Managers and "async with"
> ----------------------------------------------
>
> An *asynchronous context manager* is a context manager that is able to
> suspend
> execution in its *enter* and *exit* methods.
A regular 'with' block guarantees that subsequent code won't execute
until __exit__ completes. I presume the same guarantee is applied to
an 'async with' block, and thus it can be used only inside a
coroutine?
(Edit: Oops, missed where that's said further down - SyntaxError to
use it outside a coroutine. Might still be worth clarifying the
guarantee somewhere, otherwise ignore me.)
> New Syntax
> ''''''''''
>
> A new statement for iterating through asynchronous iterators is proposed::
>
> async for TARGET in ITER:
> BLOCK
> else:
> BLOCK2
>
> which is semantically equivalent to::
>
> iter = (ITER)
> iter = await type(iter).__aiter__(iter)
> running = True
> while running:
> try:
> TARGET = await type(iter).__anext__(iter)
> except StopAsyncIteration:
> running = False
> else:
> BLOCK
> else:
> BLOCK2
(Not sure why you don't just use "break" instead of "running = False"?
Maybe I'm blind to some distinction here.)
> Why StopAsyncIteration?
> '''''''''''''''''''''''
>
> Coroutines are still based on generators internally. So, before PEP 479,
> there
> was no fundamental difference between
>
> ::
>
> def g1():
> yield from fut
> return 'spam'
>
> and
>
> ::
>
> def g2():
> yield from fut
> raise StopIteration('spam')
>
> And since PEP 479 is accepted and enabled by default for coroutines, the
> following example will have its ``StopIteration`` wrapped into a
> ``RuntimeError``
>
> ::
>
> async def a1():
> await fut
> raise StopIteration('spam')
>
> The only way to tell the outside code that the iteration has ended is to
> raise
> something other than ``StopIteration``. Therefore, a new built-in exception
> class ``StopAsyncIteration`` was added.
>
> Moreover, with semantics from PEP 479, all ``StopIteration`` exceptions
> raised
> in coroutines are wrapped in ``RuntimeError``.
Can't a coroutine simply use 'return'? I'm not hugely familiar with
them, but I'm not understanding here why you need a completely
separate exception - and, more importantly, why you wouldn't have the
same consideration of "StopAsyncIteration leakage". It ought to be
possible for the coroutine to return, StopIteration be synthesized,
and the __anext__ function to bubble that out - exactly the way it
does for generators today.
> Glossary
> ========
>
> :Coroutine:
> A coroutine function, or just "coroutine", is declared with ``async
> def``.
> It uses ``await`` and ``return value``; see `New Coroutine Declaration
> Syntax`_ for details.
(Using this definition to justify my statement above)
> Why not a __future__ import
> ---------------------------
>
> ``__future__`` imports are inconvenient and easy to forget to add. Also,
> they
> are enabled for the whole source file. Consider that there is a big project
> with a popular module named "async.py". With future imports it is required
> to
> either import it using ``__import__()`` or ``importlib.import_module()``
> calls,
> or to rename the module. The proposed approach makes it possible to
> continue
> using old code and modules without a hassle, while coming up with a
> migration
> plan for future python versions.
The clash would occur only in modules that use the __future__ import,
wouldn't it? And those modules are going to have to be fixed by
version X.Y anyway - the whole point of the __future__ import is to
detect that.
> Why magic methods start with "a"
> --------------------------------
>
> New asynchronous magic methods ``__aiter__``, ``__anext__``, ``__aenter__``,
> and ``__aexit__`` all start with the same prefix "a". An alternative
> proposal
> is to use "async" prefix, so that ``__aiter__`` becomes ``__async_iter__``.
> However, to align new magic methods with the existing ones, such as
> ``__radd__`` and ``__iadd__`` it was decided to use a shorter version.
I support the short names, with the possible exception that
"__await__" now looks like "asynchronous wait", which may or may not
be desirable.
> Comprehensions
> --------------
>
> For the sake of restricting the broadness of this PEP there is no new syntax
> for asynchronous comprehensions. This should be considered in a separate
> PEP,
> if there is a strong demand for this feature.
Likewise asynchronous lambda functions (or maybe I just missed it).
Overall comment: The changes appear to be infecting a lot of code.
Will this suddenly be something that everyone has to think about, or
can it still be ignored in projects that don't use it? There'll be a
few object types that would benefit from becoming async context
managers as well as regular context managers (I would expect, for
instance, that the built-in file type would allow its auto-closing to
be asynchronous), but if a third-party module completely ignores this
proposal, everything should continue to work, right?
I'm seeing the potential for a lot of code duplication here, but
possibly once things settle into real-world usage, a few easy
decorators will help out with that (like the total_ordering decorator
for handling __gt__, __le__, etc etc).
+1 on the proposal as a whole.
ChrisA
More information about the Python-ideas
mailing list