Why in the example function terminates:
def func(iterable):
while True:
val = next(iterable)
yield val
but if I take off yield statement function will raise StopIteration exception?
EDIT: Sorry for misleading you guys. I know what generators are and how to use them. Of course when I said function terminates I didn't mean eager evaluation of function. I just implied that when I use function to produce generator:
gen = func(iterable)
in case of func it works and returns the same generator, but in case of func2:
def func2(iterable):
while True:
val = next(iterable)
it raises StopIteration instead of None return or infinite loop.
Let me be more specific. There is a function tee in itertools which is equivalent to:
def tee(iterable, n=2):
it = iter(iterable)
deques = [collections.deque() for i in range(n)]
def gen(mydeque):
while True:
if not mydeque: # when the local deque is empty
newval = next(it) # fetch a new value and
for d in deques: # load it to all the deques
d.append(newval)
yield mydeque.popleft()
return tuple(gen(d) for d in deques)
There is, in fact, some magic, because nested function gen has infinite loop without break statements. gen function terminates due to StopIteration exception when there is no items in it. But it terminates correctly (without raising exceptions), i.e. just stops loop. So the question is: where is StopIteration is handled?
Note: This question (and the original part of my answer to it) are only really meaningful for Python versions prior to 3.7. The behavior that was asked about no longer happens in 3.7 and later, thanks to changes described in PEP 479. So this question and the original answer are only really useful as historical artifacts. After the PEP was accepted, I added an additional section at the bottom of the answer which is more relevant to modern versions of Python.
To answer your question about where the
StopIteration
gets caught in thegen
generator created inside ofitertools.tee
: it doesn't. It is up to the consumer of thetee
results to catch the exception as they iterate.First off, it's important to note that a generator function (which is any function with a
yield
statement in it, anywhere) is fundamentally different than a normal function. Instead of running the function's code when it is called, instead, you'll just get agenerator
object when you call the function. Only when you iterate over the generator will you run the code.A generator function will never finish iterating without raising
StopIteration
(unless it raises some other exception instead).StopIteration
is the signal from the generator that it is done, and it is not optional. If you reach areturn
statement or the end of the generator function's code without raising anything, Python will raiseStopIteration
for you!This is different from regular functions, which return
None
if they reach the end without returning anything else. It ties in with the different ways that generators work, as I described above.Here's an example generator function that will make it easy to see how
StopIteration
gets raised:Here's what happens when you consume it:
Calling
simple_generator
always returns agenerator
object immediately (without running any of the code in the function). Each call ofnext
on the generator object runs the code until the nextyield
statement, and returns the yielded value. If there is no more to get,StopIteration
is raised.Now, normally you don't see
StopIteration
exceptions. The reason for this is that you usually consume generators insidefor
loops. Afor
statement will automatically callnext
over and over untilStopIteration
gets raised. It will catch and suppress theStopIteration
exception for you, so you don't need to mess around withtry
/except
blocks to deal with it.A
for
loop likefor item in iterable: do_suff(item)
is almost exactly equivalent to thiswhile
loop (the only difference being that a realfor
doesn't need a temporary variable to hold the iterator):The
gen
generator function you showed at the top is one exception. It uses theStopIteration
exception produced by the iterator it is consuming as it's own signal that it is done being iterated on. That is, rather than catching theStopIteration
and then breaking out of the loop, it simply lets the exception go uncaught (presumably to be caught by some higher level code).Unrelated to the main question, there is one other thing I want to point out. In your code, you're calling
next
on an variable callediterable
. If you take that name as documentation for what type of object you will get, this is not necessarily safe.next
is part of theiterator
protocol, not theiterable
(orcontainer
) protocol. It may work for some kinds of iterables (such as files and generators, as those types are their own iterators), but it will fail for others iterables, such as tuples and lists. The more correct approach is to calliter
on youriterable
value, then callnext
on the iterator you receive. (Or just usefor
loops, which call bothiter
andnext
for you at appropriate times!)I just found my own answer in a Google search for a related question, and I feel I should update to point out that the answer above is not true in modern Python versions.
PEP 479 has made it an error to allow a
StopIteration
to bubble up uncaught from a generator function. If that happens, Python will turn it into aRuntimeError
exception instead. This means that code like the examples in older versions ofitertools
that used aStopIteration
to break out of a generator function needs to be modified. Usually you'll need to catch the exception with atry
/except
and thenreturn
.Because this was a backwards incompatible change, it was phased in gradually. In Python 3.5, all code worked as before by default, but you could get the new behavior with
from __future__ import generator_stop
. In Python 3.6, unmodified code would still work, but it would give a warning. In Python 3.7 and later, the new behavior applies all the time.