New examples, explanations and a few edits, all by Chris.
This commit is contained in:
parent
23e1d5b765
commit
89c4b9ef1f
127
pep-0479.txt
127
pep-0479.txt
|
@ -98,6 +98,120 @@ object, and generators with the flag set will behave according to this
|
||||||
proposal. Once the feature becomes standard, the flag may be dropped;
|
proposal. Once the feature becomes standard, the flag may be dropped;
|
||||||
code should not inspect generators for it.
|
code should not inspect generators for it.
|
||||||
|
|
||||||
|
Examples
|
||||||
|
--------
|
||||||
|
|
||||||
|
Generators which explicitly raise StopIteration can generally be
|
||||||
|
changed to simply return instead. This will be compatible with all
|
||||||
|
existing Python versions, and will not be affected by __future__.
|
||||||
|
|
||||||
|
Lib/ipaddress.py::
|
||||||
|
if other == self:
|
||||||
|
raise StopIteration
|
||||||
|
Becomes::
|
||||||
|
if other == self:
|
||||||
|
return
|
||||||
|
|
||||||
|
In some cases, this can be combined with ``yield from`` to simplify
|
||||||
|
the code, such as Lib/difflib.py::
|
||||||
|
if context is None:
|
||||||
|
while True:
|
||||||
|
yield next(line_pair_iterator)
|
||||||
|
Becomes::
|
||||||
|
if context is None:
|
||||||
|
yield from line_pair_iterator
|
||||||
|
return
|
||||||
|
(The ``return`` is necessary for a strictly-equivalent translation,
|
||||||
|
though in this particular file, there is no further code, and the
|
||||||
|
``return`` can be elided.) For compatibility with pre-3.3 versions
|
||||||
|
of Python, this could be written with an explicit ``for`` loop::
|
||||||
|
if context is None:
|
||||||
|
for line in line_pair_iterator:
|
||||||
|
yield line
|
||||||
|
return
|
||||||
|
|
||||||
|
More complicated iteration patterns will need explicit try/catch
|
||||||
|
constructs. For example, a parser construct like this::
|
||||||
|
def unwrap(f):
|
||||||
|
while True:
|
||||||
|
data = next(f)
|
||||||
|
while True:
|
||||||
|
line = next(f)
|
||||||
|
if line == "- end -": break
|
||||||
|
data += line
|
||||||
|
yield data
|
||||||
|
would need to be rewritten as::
|
||||||
|
def parser(f):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
data = next(f)
|
||||||
|
while True:
|
||||||
|
line = next(f)
|
||||||
|
if line == "- end -": break
|
||||||
|
data += line
|
||||||
|
yield data
|
||||||
|
except StopIteration:
|
||||||
|
return
|
||||||
|
or possibly::
|
||||||
|
def parser(f):
|
||||||
|
for data in f:
|
||||||
|
while True:
|
||||||
|
line = next(f)
|
||||||
|
if line == "- end -": break
|
||||||
|
data += line
|
||||||
|
yield data
|
||||||
|
|
||||||
|
The latter form obscures the iteration by purporting to iterate over
|
||||||
|
the file with a ``for`` loop, but then also fetches more data from
|
||||||
|
the same iterator during the loop body. It does, however, clearly
|
||||||
|
differentiate between a "normal" termination (``StopIteration``
|
||||||
|
instead of the initial line) and an "abnormal" termination (failing
|
||||||
|
to find the end marker in the inner loop, which will now raise
|
||||||
|
``RuntimeError``).
|
||||||
|
|
||||||
|
|
||||||
|
Explanation of generators, iterators, and StopIteration
|
||||||
|
=======================================================
|
||||||
|
|
||||||
|
Under this proposal, generators and iterators would be distinct, but
|
||||||
|
related, concepts. Like the mixing of text and bytes in Python 2,
|
||||||
|
the mixing of generators and iterators has resulted in certain
|
||||||
|
perceived conveniences, but proper separation will make bugs more
|
||||||
|
visible.
|
||||||
|
|
||||||
|
An iterator is an object with a ``__next__`` method. Like many other
|
||||||
|
dunder methods, it may either return a value, or raise a specific
|
||||||
|
exception - in this case, ``StopIteration`` - to signal that it has
|
||||||
|
no value to return. In this, it is similar to ``__getattr__`` (can
|
||||||
|
raise ``AttributeError``), ``__getitem__`` (can raise ``KeyError``),
|
||||||
|
and so on. A helper function for an iterator can be written to
|
||||||
|
follow the same protocol; for example::
|
||||||
|
def helper(x, y):
|
||||||
|
if x > y: return 1 / (x - y)
|
||||||
|
raise StopIteration
|
||||||
|
def __next__(self):
|
||||||
|
if self.a: return helper(self.b, self.c)
|
||||||
|
return helper(self.d, self.e)
|
||||||
|
|
||||||
|
Both forms of signalling are carried through: a returned value is
|
||||||
|
returned, an exception bubbles up. The helper is written to match
|
||||||
|
the protocol of the calling function.
|
||||||
|
|
||||||
|
A generator function is one which contains a ``yield`` expression.
|
||||||
|
Each time it is (re)started, it may either yield a value, or return
|
||||||
|
(including "falling off the end"). A helper function for a generator
|
||||||
|
can also be written, but it must also follow generator protocol::
|
||||||
|
def helper(x, y):
|
||||||
|
if x > y: yield 1 / (x - y)
|
||||||
|
def gen(self):
|
||||||
|
if self.a: return (yield from helper(self.b, self.c))
|
||||||
|
return (yield from helper(self.d, self.e))
|
||||||
|
|
||||||
|
In both cases, any unexpected exception will bubble up. Due to the
|
||||||
|
nature of generators and iterators, an unexpected ``StopIteration``
|
||||||
|
inside a generator will be converted into ``RuntimeError``, but
|
||||||
|
beyond that, all exceptions will propagate normally.
|
||||||
|
|
||||||
|
|
||||||
Alternate proposals
|
Alternate proposals
|
||||||
===================
|
===================
|
||||||
|
@ -137,17 +251,20 @@ The inspiration for this alternative proposal was Nick's observation
|
||||||
[7]_ that if an ``asyncio`` coroutine [8]_ accidentally raises
|
[7]_ that if an ``asyncio`` coroutine [8]_ accidentally raises
|
||||||
``StopIteration``, it currently terminates silently, which may present
|
``StopIteration``, it currently terminates silently, which may present
|
||||||
a hard-to-debug mystery to the developer. The main proposal turns
|
a hard-to-debug mystery to the developer. The main proposal turns
|
||||||
such accidents in clearly distinguishable ``RuntimeError`` exceptions,
|
such accidents into clearly distinguishable ``RuntimeError`` exceptions,
|
||||||
but if that is rejected, this alternate proposal would enable
|
but if that is rejected, this alternate proposal would enable
|
||||||
``asyncio`` to distinguish between a ``return`` statement and an
|
``asyncio`` to distinguish between a ``return`` statement and an
|
||||||
accidentally-raised ``StopIteration`` exception.
|
accidentally-raised ``StopIteration`` exception.
|
||||||
|
|
||||||
Of the three outcomes listed above:
|
Of the three outcomes listed above, two change:
|
||||||
|
|
||||||
* A yielded value, obviously, would still be returned.
|
* If a yield point is reached, the value, obviously, would still be
|
||||||
* If the frame is returned from, ``GeneratorReturn`` is raised.
|
returned.
|
||||||
|
* If the frame is returned from, ``GeneratorReturn`` (rather than
|
||||||
|
``StopIteration``) is raised.
|
||||||
* If an instance of ``GeneratorReturn`` would be raised, instead an
|
* If an instance of ``GeneratorReturn`` would be raised, instead an
|
||||||
instance of ``StopIteration`` would be raised.
|
instance of ``StopIteration`` would be raised. Any other exception
|
||||||
|
bubbles up normally.
|
||||||
|
|
||||||
In the third case, the ``StopIteration`` would have the ``value`` of
|
In the third case, the ``StopIteration`` would have the ``value`` of
|
||||||
the original ``GeneratorReturn``, and would reference the original
|
the original ``GeneratorReturn``, and would reference the original
|
||||||
|
|
Loading…
Reference in New Issue