@@ -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==========
@@ -105,8 +108,8 @@ in asynchronous generators:
1051082. https://discuss.python.org/t/47050
1061093. https://discuss.python.org/t/66886
107110
108- Additionally, this design decision has ` come up
109- <https://stackoverflow.com/questions/47376408> `__ on Stack Overflow.
111+ Additionally, users have ` questioned < https://stackoverflow.com/questions/47376408 >`__
112+ this design decision on Stack Overflow.
110113
111114
112115Subgenerator delegation is useful for asynchronous generators
@@ -119,16 +122,17 @@ item. This comes with a few drawbacks:
1191221. It obscures the intent of the code and increases the amount of effort
120123 necessary to work with asynchronous generators, because each delegation
121124 point becomes a loop. This damages the power of asynchronous generators.
122- 2. :meth: `~agen.asend `, :meth: `~agen.athrow `, and :meth: `~agen.aclose `,
125+ 2. :meth: `~agen.asend `, :meth: `~agen.athrow `, and :meth: `~agen.aclose `
123126 do not interact properly with the caller. This is the primary reason that
124127 ``yield from `` was added in the first place.
1251283. Return values are not natively supported with asynchronous generators. The
126- workaround for this it to raise an exception, which increases boilerplate.
129+ workaround for this is to raise an exception, which increases boilerplate.
127130
128131
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,87 @@ 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. As such,
378+ it was proposed to make ``yield from `` always delegate to an asynchronous
379+ subgenerator.
380+
381+ For example:
382+
383+ .. code-block :: python
384+
385+ async def asubgenerator ():
386+ yield 1
387+ yield 2
388+
389+ async def agenerator ():
390+ yield from asubgenerator()
391+
392+
393+ This was rejected, primarily because it felt very wrong for ``yield from x `` to
394+ be valid or invalid depending on the type of generator it was used in.
395+
396+ In addition, there is no precedent for this kind of behavior in Python; inherently
397+ synchronous constructs always have an asynchronous counterpart for use in
398+ asynchronous functions, instead of implicitly switching protocols depending on
399+ the type of function it is used in. For example, :keyword: `with ` always means that the
400+ :term: `synchronous context management protocol <context management protocol> ` will
401+ be invoked, even when used in an ``async def `` function.
402+
403+ Finally, this would leave a gap in asynchronous generators, because there would be
404+ no mechanism for delegating to a synchronous subgenerator. Even if this is not a
405+ common pattern today, this may become common in the future, in which case it would
406+ be very difficult to change the meaning of ``yield from `` in an asynchronous
407+ generator.
408+
409+
410+ Letting ``yield from `` determine which protocol to use
411+ ------------------------------------------------------
412+
413+ As a solution to the above rejected idea, it was proposed to allow ``yield from x ``
414+ to invoke the synchronous or asynchronous generator protocol depending on the type
415+ of ``x ``. In turn, this would allow developers to delegate to both synchronous
416+ and asynchronous subgenerators while continuing to use the familiar ``yield from ``
417+ syntax.
418+
419+ For example:
420+
421+ .. code-block :: python
422+
423+ async def asubgenerator ():
424+ yield 1
425+ yield 2
426+
427+ async def agenerator ():
428+ yield from asubgenerator()
429+ yield from range (3 , 5 )
430+
431+
432+ Mechanically, this is possible, but the exact behavior will likely be counterintuitive
433+ and ambigious. In particular:
434+
435+ 1. If an object implements both :meth: `~object.__iter__ ` and :meth: `~object.__aiter__ `,
436+ it's not clear which protocol Python should choose.
437+ 2. If the chosen protocol raises an exception, should the exception be propagated, or
438+ should Python try to use the other protocol first?
439+
440+ Additionally, this approach is inherently slower, because of the additional overhead
441+ of detecting which generator protocol to use.
340442
341443
342444Acknowledgements
343445================
344446
345447Thanks to Bartosz Sławecki for aiding in the development of the reference
346448implementation of this PEP. In addition, the :exc: `StopAsyncIteration `
347- changes in addition to the support for non-``None `` return values inside
449+ changes alongside the support for non-``None `` return values inside
348450asynchronous generators were largely based on Alex Dixon's design from
349- `python/cpython#125401 <https://github.com/python/cpython/pull/125401 >`__
451+ `python/cpython#125401 <https://github.com/python/cpython/pull/125401 >`__.
350452
351453
352454Change History
0 commit comments