Search code examples
pythonpython-3.xiteratorgeneratoryield

StopIteration exception handling raised in generator function in python


Here's is my generator function code:

def fibo():
    n1=0
    n2=1
    while True:
        if n1 == 8:
            raise StopIteration
        else:
            yield n1
            n1,n2=n2,n1+n2
        
seq=fibo()

Here is my code version-1 for generating sequence:

for ele in seq:
    print(next(seq))

Output:

1
2
5

RuntimeError: generator raised StopIteration
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-3-c01815b93e23> in fibo()
      5         if n1 == 8:
----> 6             raise StopIteration
      7         else:

StopIteration: 

The above exception was the direct cause of the following exception:

RuntimeError                              Traceback (most recent call last)
<ipython-input-3-c01815b93e23> in <module>
     11 seq=fibo()
     12 
---> 13 for ele in seq:
     14     print(next(seq))
     15 

RuntimeError: generator raised StopIteration

My expected output for version-1:

0
1
1
2
3
5

Here is my code version-2 for generating sequence:

while True:
    try:
        print(next(seq))
    except StopIteration:
        print('This exception is raised in the generator operation defining function. Hence it is handled here.')
        break

Output:

0
1
1
2
3
5
RuntimeError: generator raised StopIteration
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-4-6afe26583a33> in fibo()
      5         if n1 == 8:
----> 6             raise StopIteration
      7         else:

StopIteration: 

The above exception was the direct cause of the following exception:

RuntimeError                              Traceback (most recent call last)
<ipython-input-4-6afe26583a33> in <module>
     16 while True:
     17     try:
---> 18         print(next(seq))
     19     except StopIteration:
     20         print('This exception is raised in the generator operation defining function. Hence it is handled here.')

RuntimeError: generator raised StopIteration

My expected output for version-2:

0
1
1
2
3
5
This exception is raised in the generator operation defining function. Hence it is handled here.

In the first version, out is incorrect and exception is happening. In the second, in addition to correct output, same exception as version-1 is happening. Please help me to rectify this.


Solution

  • Here is my code version-1 for generating sequence:

    The point of the for loop construct in Python is that it already calls next on the iterator (which it creates from the generator automatically). So you should just print(ele) instead.

    RuntimeError: generator raised StopIteration

    Yes, because you are aren't supposed to use explicit raise StopIteration in generators. That's an old tool for communicating to the for loop that you're out of elements, which is only needed when you implement an __iter__ method the old-fashioned way. Yes, the for loop construct depends on this exception to determine the end of the input; but the iterator created from your generator will do this automatically - and also check for StopIterations raised in the generator and convert them to RuntimeError, explicitly to prevent your StopIteration from breaking out of loops.

    If the underlying machinery didn't have that check, then you could do things like for i in itertools.chain(my_generator, a_list):, and the exception raised by the generator implementation would forcibly break the loop (after all, the loop exits by handling that exception) and prevent the a_list elements from being seen. That's not how you want the generator to behave, and it's also in some sense a security risk, so that check is there.

    If you want to signal that a generator is done yielding, what you do is... get to the end of the code. For example, with return, or in this case break also works (since there is nothing else after the loop):

    def fibo():
        n1, n2 = 0, 1
        while True:
            if n1 == 8:
                break
            else:
                yield n1
                n1, n2 = n2, n1+n2
    

    print('This exception is raised in the generator operation defining function. Hence it is handled here.')

    The reason that doesn't happen is that you are trying to catch StopIteration (which you should not do because it would interfere with the loop's operation!), but the exception was (which you would know if you carefully read the stack trace) replaced with RuntimeError instead. See above.

    With the corrected code above, everything works as expected:

    >>> list(fibo())
    [0, 1, 1, 2, 3, 5]
    
    >>> x = fibo()
    >>> for f in x:
    ...   print(f)
    ...
    0
    1
    1
    2
    3
    5
    
    >>> x = fibo()
    >>> while True:
    ...   try:
    ...     print(next(x))
    ...   except StopIteration:
    ...     print('this works as intended - but a for loop is much cleaner')
    ...     break
    ...
    0
    1
    1
    2
    3
    5
    this works as intended - but a for loop is much cleaner