[Python-ideas] Generators are iterators

Terry Reedy tjreedy at udel.edu
Fri Dec 12 21:06:11 CET 2014


On 12/12/2014 7:42 AM, Oscar Benjamin wrote:> On 12 December 2014 at 
10:34, Nick Coghlan <ncoghlan at gmail.com> 
wrote:

 >> specific problem deemed worthy of being fixed is that the presence of
 >> "yield" in a function body can implicitly suppress StopIteration
 >> exceptions raised elsewhere in that function body (or in functions it
 >> calls).

This makes no sense to me either.

 > The yield causes the function to become a generator function. The
 > frame for a generator function (like for any other function) will
 > allow uncaught exceptions to propagate to the frame above. The
 > difference between generator functions and other functions is that the
 > code in the body of a generator function is executed when someone
 > calls the generator's __next__ method. Since the caller (the iterator
 > consumer) is expecting StopIteration it is treated as the signalling
 > the end of iteration. The yield suppresses nothing; it is the iterator
 > consumer e.g. the for-loop or the list() function etc. which catches
 > the StopIteration and treats it as termination.
 >
 >> The difference in behaviour between comprehensions and
 >> generator expressions when it comes to embedded function calls that
 >> trigger StopIteration is a special case of that more general
 >> difference.
 >
 > I don't know what you mean by this.

Neither do I

 >> This is a problem unique to generators, it does not affect any other
 >> iterator (since explicit __next__ method implementations do not use
 >> yield).

The parenthetical comment is true.  However, I think part of the problem 
is unique to generator functions, and part is more general -- that loose 
usage of StopIteration creates problems when a .__next__ method calls 
external code.

 > Incorrect. The problem is not unique to generators and the yield is
 > irrelevant.

Nick (and Guido) are looking at the unique part and Oscar is looking at 
he general part.

 > The problem (insofar as it is) is a problem for all
 > iterators since all iterators interact with iterator-consumers and it
 > is the iterator-consumer that catches the StopIteration. Here is an
 > example using map:
 >
 >>>> def func(x):
 > ...     if x < 0:
 > ...         raise StopIteration
 > ...     return x ** 2

This function violates the implicit guideline, which I think should be 
more explicit in the doc, that the only functions that should expose 
StopIteration to the outside world are iterator.__next__ functions and 
the corresponding builtin next().  It is not surprising that this 
violation causes map to misbehave.  However, changing the .__next__ 
methods of map and filter iterators should be a new thread, which I will 
write after posting this.

The g.f. PEP says that generator functions should also follow this 
guideline and not raise StopIteration (but should always return when not 
yielding).

...
 >> The change in the PEP is to change that side effect such that
 >> those exceptions are converted to RuntimeError rather than silently
 >> suppressed - making generator function bodies behave less like
 >> __next__ method implementations.
 >
 > Fine but generator function bodies are __next__ method
 > implementations.

This is about as wrong, or at least as confusing, as saying that the 
function passed to map is a __next__ method implementation.  There is 
only one generator.__next__, just as there is only one map.__next__. 
Both .__next__ methods execute the user function (or function frame) 
passed in to the corresponding .__init__ to calculate the next value to 
return.  So the external functions assist the __next__ methods but are 
not __next__ methods themselves.  In particular, they should not raise 
StopIteration.

-- 
Terry Jan Reedy



More information about the Python-ideas mailing list