Skip to content

Commit 13e1c21

Browse files
authored
Merge branch 'main' into parameters
2 parents 30c4c1e + 78e1527 commit 13e1c21

File tree

2 files changed

+142
-13
lines changed

2 files changed

+142
-13
lines changed

peps/pep-0820.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,33 @@ Limited forward compatibility
100100
versions.
101101

102102

103+
Endorsements
104+
------------
105+
106+
This PEP is unusual in that it doesn't immediately help end users -- it needs
107+
to be in place well before it starts being helpful.
108+
In the words of a Cython maintainer,
109+
`Da Woods <https://discuss.python.org/t/105552/2>`__:
110+
111+
I suspect Cython wouldn’t immediately use it (at least for classes… obviously
112+
if it goes into PEP 793 then we would for that).
113+
Just because it’s mostly targeted at future improvements rather than an
114+
immediate new feature. It looks usable though.
115+
116+
Instead, support comes from core devs who'll need change the C API, and want
117+
do so in backwards-compatible ways:
118+
119+
- `Mark Shannon <https://discuss.python.org/t/105552/4>`__:
120+
121+
This seems like a worthwhile improvement. Count me as a +1
122+
123+
- `Victor Stinner <https://discuss.python.org/t/105552/19>`__:
124+
125+
I changed my mind and I’m now a supporter of PEP 820 :)
126+
It took me a while but I now see the PEP 820 advantages: it enhances the
127+
backward compatibility and the stable ABI.
128+
129+
103130
Example
104131
=======
105132

peps/pep-0828.rst

Lines changed: 115 additions & 13 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
==========
@@ -105,8 +108,8 @@ in asynchronous generators:
105108
2. https://discuss.python.org/t/47050
106109
3. 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

112115
Subgenerator delegation is useful for asynchronous generators
@@ -119,16 +122,17 @@ item. This comes with a few drawbacks:
119122
1. 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.
125128
3. 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

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,87 @@ 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. 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

342444
Acknowledgements
343445
================
344446

345447
Thanks to Bartosz Sławecki for aiding in the development of the reference
346448
implementation 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
348450
asynchronous 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

352454
Change History

0 commit comments

Comments
 (0)