I'm reading the documentation for Python 3 here:
If a generator code directly or indirectly raises
StopIteration, it is converted into aRuntimeError(retaining theStopIterationas the new exception's cause).
I don't understand that, can anyone explain?
This is what I've tried in Python 3.6, but nothing seems to have been caught:
def gen1():
yield from [1, 2, 3]
raise StopIteration
def gen2():
raise StopIteration
try:
a = list(gen1())
# a == [1, 2, 3]
except RuntimeError:
print("Caught")
try:
a = gen1()
next(a), next(a), next(a), next(a), next(a)
except RuntimeError:
print("Caught")
try:
gen2()
except RuntimeError:
print("Caught")
try:
a = list(gen2())
except RuntimeError:
print("Caught")
Specially, both calls to gen2() raised the StopIteration, but still not converted into RuntimeError.
You missed that this change applies to Python 3.7 and newer. You won't see the conversion in Python 3.6 or older, unless you enable the feature with a from __future__ import first (available as of Python 3.5).
From the same page you linked:
Changed in version 3.5: Introduced the
RuntimeErrortransformation viafrom __future__ import generator_stop, see PEP 479.Changed in version 3.7: Enable PEP 479 for all code by default: a
StopIterationerror raised in a generator is transformed into aRuntimeError.
PEP 479 -- Change StopIteration handling inside generators further details why this change was made and how it applies. For your code, running on Python 3.7, the output becomes:
>>> import sys
>>> sys.version_info
sys.version_info(major=3, minor=7, micro=0, releaselevel='final', serial=0)
>>> def gen1():
... yield from [1, 2, 3]
... raise StopIteration
...
>>> def gen2():
... yield 42 # make this an actual generator
... raise StopIteration
...
>>> try:
... a = list(gen1())
... except RuntimeError:
... print("Caught")
...
Caught
>>> try:
... a = gen1()
... next(a), next(a), next(a), next(a), next(a)
... except RuntimeError:
... print("Caught")
...
Caught
>>> try:
... a = list(gen2())
... except RuntimeError:
... print("Caught")
...
Caught
Note that I added a yield 42 line to gen2() to make it a generator. Without yield or yield from in the body, you get a regular function instead. Calling a generator function produces a generator object and the function body starts out paused, while calling a normal function executes the body immediately:
>>> def normal():
... raise StopIteration
...
>>> def generator():
... raise StopIteration
... yield # never reached, but this is now a generator
...
>>> normal()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in normal
StopIteration
>>> generator()
<generator object generator at 0x105831ed0>
>>> next(generator())
Traceback (most recent call last):
File "<stdin>", line 2, in generator
StopIteration
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: generator raised StopIteration
For Python 3.6, you'd use the from __future__ import generator_stop compiler switch (use it at the top of your code when writing a script or module):
>>> import sys
>>> sys.version_info
sys.version_info(major=3, minor=6, micro=5, releaselevel='final', serial=0)
>>> def generator():
... raise StopIteration
... yield
...
>>> next(generator())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in generator
StopIteration
>>> from __future__ import generator_stop
>>> def generator(): # re-define it so it is compiled anew
... raise StopIteration
... yield
...
>>> next(generator())
Traceback (most recent call last):
File "<stdin>", line 2, in generator
StopIteration
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
RuntimeError: generator raised StopIteration
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