Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When will a StopIteration be converted into RuntimeError?

I'm reading the documentation for Python 3 here:

If a generator code directly or indirectly raises StopIteration, it is converted into a RuntimeError (retaining the StopIteration as 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.

like image 895
iBug Avatar asked Aug 06 '18 16:08

iBug


1 Answers

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 RuntimeError transformation via from __future__ import generator_stop, see PEP 479.

Changed in version 3.7: Enable PEP 479 for all code by default: a StopIteration error raised in a generator is transformed into a RuntimeError.

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
like image 131
Martijn Pieters Avatar answered Oct 21 '22 20:10

Martijn Pieters