I recently came across some surprising behaviour in Python generators:
class YieldOne:
def __iter__(self):
try:
yield 1
except:
print '*Excepted Successfully*'
# raise
for i in YieldOne():
raise Exception('test exception')
Which gives the output:
*Excepted Successfully*
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
Exception: test exception
I was (pleasantly) surprised that *Excepted Successfully*
got printed, as this was what I wanted, but also surprised that the Exception still got propagated up to the top level. I was expecting to have to use the (commented in this example) raise
keyword to get the observed behaviour.
Can anyone explain why this functionality works as it does, and why the except
in the generator doesn't swallow the exception?
Is this the only instance in Python where an except
doesn't swallow an exception?
Python's generator functions are almost coroutines – but not quite – in that they allow pausing execution to produce a value, but do not provide for values or exceptions to be passed in when execution resumes.
Coroutines are Generators, but their yield accepts values. Coroutines can pause and resume execution (great for concurrency).
In the previous example, we stop the iteration by raising an exception, however, that's not very elegant. A better way to end the iterations is by using . close() . In this case, the generator stopped and we left the loop without raising any exception.
The GeneratorExit error occurs in Python because of the sudden exit of the generator function. The main difference between Generators and normal functions is that the normal function uses the return keyword to return the values from the function, but in the generator, the yield method is used to return the values.
Your code does not do what you think it does. You cannot raise Exceptions in a coroutine like this. What you do instead is catching the GeneratorExit
exception. See what happens when you use a different Exception:
class YieldOne:
def __iter__(self):
try:
yield 1
except RuntimeError:
print "you won't see this"
except GeneratorExit:
print 'this is what you saw before'
# raise
for i in YieldOne():
raise RuntimeError
As this still gets upvotes, here is how you raise an Exception in a generator:
class YieldOne:
def __iter__(self):
try:
yield 1
except Exception as e:
print "Got a", repr(e)
yield 2
# raise
gen = iter(YieldOne())
for row in gen:
print row # we are at `yield 1`
print gen.throw(Exception) # throw there and go to `yield 2`
See docs for generator.throw
.
EDIT: What THC4k said.
If you really want to raise an arbitrary exception inside a generator, use the throw
method:
>>> def Gen():
... try:
... yield 1
... except Exception:
... print "Excepted."
...
>>> foo = Gen()
>>> next(foo)
1
>>> foo.throw(Exception())
Excepted.
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
You'll notice that you get a StopIteration
at the top level. These are raised by generators which have run out of elements; they are usually swallowed by the for
loop but in this case we made the generator raise an exception so the loop doesn't notice them.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With