New examples, explanations and a few edits, all by Chris.

This commit is contained in:
Guido van Rossum 2014-11-20 21:07:18 -08:00
parent 23e1d5b765
commit 89c4b9ef1f
1 changed files with 122 additions and 5 deletions

View File

@ -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;
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
===================
@ -137,17 +251,20 @@ The inspiration for this alternative proposal was Nick's observation
[7]_ that if an ``asyncio`` coroutine [8]_ accidentally raises
``StopIteration``, it currently terminates silently, which may present
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
``asyncio`` to distinguish between a ``return`` statement and an
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 the frame is returned from, ``GeneratorReturn`` is raised.
* If a yield point is reached, the value, obviously, would still be
returned.
* If the frame is returned from, ``GeneratorReturn`` (rather than
``StopIteration``) is raised.
* 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
the original ``GeneratorReturn``, and would reference the original