Skip to content

Commit 8dbd31e

Browse files
Add some clarification
1 parent 63c2676 commit 8dbd31e

File tree

1 file changed

+110
-9
lines changed

1 file changed

+110
-9
lines changed

peps/pep-0828.rst

Lines changed: 110 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ For example, the following code is valid under this PEP:
2828
yield from generator()
2929
3030
31-
32-
In addition, this PEP introduces a new ``async yield from`` syntax to use
33-
existing ``yield from`` semantics on an asynchronous generator:
31+
In addition, this PEP introduces a new ``async yield from`` construct to
32+
delegate to an asynchronous generator:
3433

3534
.. code-block:: python
3635
@@ -67,6 +66,10 @@ as an :term:`asynchronous generator`, sometimes suffixed with "function".
6766
In contrast, the object returned by an asynchronous generator is referred to
6867
as an :term:`asynchronous generator iterator` in this PEP.
6968

69+
This PEP also uses the term "subgenerator" to refer to a generator, synchronous
70+
or asynchronous, that is used inside of a ``yield from`` or ``async yield from``
71+
expression.
72+
7073

7174
Motivation
7275
==========
@@ -129,6 +132,7 @@ item. This comes with a few drawbacks:
129132
Specification
130133
=============
131134

135+
132136
Syntax
133137
------
134138

@@ -163,7 +167,7 @@ This PEP retains all existing ``yield from`` semantics; the only detail is
163167
that asynchronous generators may now use it.
164168

165169
Because the existing ``yield from`` behavior may only yield from a synchronous
166-
generator, this is true for asynchronous generators as well.
170+
subgenerator, this is true for asynchronous generators as well.
167171

168172
For example:
169173

@@ -294,6 +298,7 @@ knowledge of ``yield from`` in synchronous generators.
294298
Potential footguns
295299
------------------
296300

301+
297302
Forgetting to ``await`` a future
298303
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
299304

@@ -314,18 +319,45 @@ For example:
314319
await asyncio.sleep(0.25)
315320
return [1, 2, 3]
316321
317-
async def generator():
322+
async def agenerator():
318323
# Forgot to await!
319324
yield from asyncio.ensure_future(steps())
320325
321326
async def run():
322327
total = 0
323-
async for i in generator():
328+
async for i in agenerator():
324329
# TypeError?!
325330
total += i
326331
print(total)
327332
328333
334+
Attempting to use ``yield from`` on an asynchronous subgenerator
335+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
336+
337+
A common intuition among developers is that ``yield from`` inside an
338+
asynchronous generator will also delegate to another asynchronous generator.
339+
As such, many users were surprised to see that, in this proposal, the following
340+
code is invalid:
341+
342+
.. code-block:: python
343+
344+
async def asubgenerator():
345+
yield 1
346+
yield 2
347+
348+
async def agenerator():
349+
yield from asubgenerator()
350+
351+
352+
As a solution, when ``yield from`` is given an object that is not iterable,
353+
the implementation can detect if that object is asynchronously iterable.
354+
If it is, ``async yield from`` can be suggested in the exception message.
355+
356+
This is done in the reference implementation of this proposal; the example
357+
above raises a :exc:`TypeError` that reads ``async_generator object is not
358+
iterable. Did you mean 'async yield from'?``
359+
360+
329361
Reference Implementation
330362
========================
331363

@@ -336,17 +368,86 @@ A reference implementation of this PEP can be found at
336368
Rejected Ideas
337369
==============
338370

339-
TBD.
371+
372+
Using ``yield from`` to delegate to asynchronous generators
373+
-----------------------------------------------------------
374+
375+
It has been argued that many developers may intuitively believe that using a
376+
plain ``yield from`` inside an asynchronous generator would also delegate to
377+
an asynchronous subgenerator rather than a synchronous subgenerator, so it was
378+
proposed to make ``yield from`` always delegate to an asynchronous subgenerator.
379+
380+
For example:
381+
382+
.. code-block:: python
383+
384+
async def asubgenerator():
385+
yield 1
386+
yield 2
387+
388+
async def agenerator():
389+
yield from asubgenerator()
390+
391+
392+
This was rejected, primarily because it felt very wrong for ``yield from x`` to
393+
be valid or invalid depending on the type of generator it was used in.
394+
395+
In addition, there is no precedent for this kind of behavior in Python; inherently
396+
synchronous constructs always have an asynchronous counterpart for use in
397+
asynchronous functions, instead of implicitly switching protocols depending on
398+
the type of function it is used in. For example, :keyword:`with` always means that the
399+
:term:`synchronous context management protocol <context management protocol>` will
400+
be invoked.
401+
402+
Finally, this would leave a gap in asynchronous generators, because there would be
403+
no mechanism for delegating to a synchronous subgenerator. Even if this is not a
404+
common pattern today, this may become common in the future, in which case it would
405+
be very difficult to change the meaning of ``yield from`` in an asynchronous
406+
generator.
407+
408+
409+
Letting ``yield from`` determine which protocol to use
410+
------------------------------------------------------
411+
412+
As a solution to the above rejected idea, it was proposed to allow ``yield from x``
413+
to invoke the synchronous or asynchronous generator protocol depending on the type
414+
of ``x``. In turn, this would allow developers to delegate to both synchronous
415+
and asynchronous subgenerators while continuing to use the familiar ``yield from``
416+
syntax.
417+
418+
For example:
419+
420+
.. code-block:: python
421+
422+
async def asubgenerator():
423+
yield 1
424+
yield 2
425+
426+
async def agenerator():
427+
yield from asubgenerator()
428+
yield from range(3, 5)
429+
430+
431+
Mechanically, this is possible, but the exact behavior will likely be counterintuitive
432+
and ambigious. In particular:
433+
434+
1. If an object implements both :meth:`~object.__iter__` and :meth:`~object.__aiter__`,
435+
it's not clear which protocol Python should choose.
436+
2. If the chosen protocol raises an exception, should the exception be propagated, or
437+
should Python try to use the other protocol first?
438+
439+
Additionally, this approach is inherently slower, because of the additional overhead
440+
of detecting which generator protocol to use.
340441

341442

342443
Acknowledgements
343444
================
344445

345446
Thanks to Bartosz Sławecki for aiding in the development of the reference
346447
implementation of this PEP. In addition, the :exc:`StopAsyncIteration`
347-
changes in addition to the support for non-``None`` return values inside
448+
changes alongside the support for non-``None`` return values inside
348449
asynchronous generators were largely based on Alex Dixon's design from
349-
`python/cpython#125401 <https://github.com/python/cpython/pull/125401>`__
450+
`python/cpython#125401 <https://github.com/python/cpython/pull/125401>`__.
350451

351452

352453
Change History

0 commit comments

Comments
 (0)