@@ -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".
6766In contrast, the object returned by an asynchronous generator is referred to
6867as 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
7174Motivation
7275==========
@@ -129,6 +132,7 @@ item. This comes with a few drawbacks:
129132Specification
130133=============
131134
135+
132136Syntax
133137------
134138
@@ -163,7 +167,7 @@ This PEP retains all existing ``yield from`` semantics; the only detail is
163167that asynchronous generators may now use it.
164168
165169Because 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
168172For example:
169173
@@ -294,6 +298,7 @@ knowledge of ``yield from`` in synchronous generators.
294298Potential footguns
295299------------------
296300
301+
297302Forgetting 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+
329361Reference Implementation
330362========================
331363
@@ -336,17 +368,86 @@ A reference implementation of this PEP can be found at
336368Rejected 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
342443Acknowledgements
343444================
344445
345446Thanks to Bartosz Sławecki for aiding in the development of the reference
346447implementation 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
348449asynchronous 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
352453Change History
0 commit comments