8

There are several ways to break out of a few nested loops

They are:

1) to use break-continue

for x in xrange(10):
    for y in xrange(10):
        print x*y
        if x*y > 50:
            break
    else:
        continue  # only executed if break was not used
    break

2) to use return

def foo():
    for x in range(10):
        for y in range(10):
            print x*y
            if x*y > 50:
                return
foo()

3) to use special exception

class BreakIt(Exception): pass

try:
    for x in range(10):
        for y in range(10):
            print x*y
            if x*y > 50:
                raise BreakIt
except BreakIt:
    pass

I had some thought that there could be some other way to do it. It is by using StopIteration exception sent directly to the outer loop. I wrote this code

it = iter(range(10))
for i in it:
    for j in range(10):
        if i*j == 20:
            raise StopIteration

Unfortunately, StopIteration hadn't been caught by any for-loop and that code produced an ugly Traceback. I think it's because StopIteration wasn't sent from inside of iterator it. (that's my guess, I'm not sure about it).

Is there any way that I can send StopIteration to the outer loop?

Thanks!

twasbrillig
  • 17,084
  • 9
  • 43
  • 67
ovgolovin
  • 13,063
  • 6
  • 47
  • 78

4 Answers4

4

Another approach to nested loops you wish to break from, is to collapse them. So something like

for x, y in ((x, y) for x in range(10) for y in range(10)):
    print x*y
    if x*y > 50: break
donkopotamus
  • 22,114
  • 2
  • 48
  • 60
  • Yeah. It can even be written this way: from itertools import product >>> for i, j, k in product(range(5), range(6), range(7)): ... pass I'm just interested if there is any way I can catch alter StopIteration so that it can be caught by particular for-loop. – ovgolovin Aug 03 '11 at 00:25
4

You can do something like this with coroutines:

def stoppable_iter(iterable):
    it = iter(iterable)
    for v in it:
        x = yield v
        if x:
            yield
            return

And then use it like this:

it = stoppable_iter(range(10))
for i in it:
    for j in range(10):
        print i, j
        if i*j == 20:
            it.send(StopIteration) # or any value that evaluates as True
            break

And a brief example of how it works:

>>> t = stoppable_iter(range(10))
>>> t.next()
0
>>> t.next()
1
>>> t.send(StopIteration)
>>> t.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
Andrew Clark
  • 202,379
  • 35
  • 273
  • 306
  • Please, can you explain, how soppable_iter works? I don't understand the part beginning with if x: ... – ovgolovin Aug 03 '11 at 00:41
  • That is really clever and much shorter than the class version I provided! – kindall Aug 03 '11 at 00:52
  • I think I need a closer look on coroutines, because with my current knowledge of it I can't grasp what does the algorithm in stoppable_iter. – ovgolovin Aug 03 '11 at 00:54
  • And if we had 3 nested loops, we would need to write: it1.send(StopIteration) it2.send(StipIteration) break Right? – ovgolovin Aug 03 '11 at 00:56
  • If `next` is called on the generator that `stoppable_iter` returns then `x` will be `None` and `v` will be returned. If `send` is called then `x` will be the value that was sent, which will cause the generator to finish as long as a non-false value is sent. – Andrew Clark Aug 03 '11 at 01:01
  • Why do we need `yield` statement after `if x:` ? – ovgolovin Aug 03 '11 at 01:10
  • Because otherwise `Stopiteration` will get raised when you `return`. Also, note you don't need to `it = iter(iterable)` then `for v in it:`, you can just `for v in iterable:` and it will still work fine. – agf Aug 03 '11 at 02:57
  • Actually, it can be even shorter: `if (yield v):` works fine, no need for `x = yield v` then `if x:` – agf Aug 03 '11 at 03:32
  • Thanks! I can say that the solution we have come to is not very practical (since there are shorter way of achieving the same behaviour of getting out of nested for-loops). But I believe it has been a good excercise for the brain and I hope it comes in handy in some other setting. – ovgolovin Aug 03 '11 at 11:52
1

You can use .close, which every generator have since Python 2.5:

Code is in Python 3.2, but it should work in 2.x as well.
In Python 2.x I'd use xrange instead of range.

outer_loop_iterator = (i for i in range(10)) #we need named generator
for x in outer_loop_iterator:
    for y in range(10):
        print(x*y)
        if x*y > 50:
            outer_loop_iterator.close()
            break #I'm affraid that without this inner loop could still work
GingerPlusPlus
  • 5,336
  • 1
  • 29
  • 52
1

I think it's because StopIteration wasn't sent from inside of iterator it. (that's my guess, I'm not sure about it).

Exactly right.

Is there any way that I can send StopIteration to the other loop?

Same way as your #3, except using StopIteration instead of an exception you define. It's a good one to use anyway.

In the comments I mentioned writing an iterator that can be told to raise StopIteration the next time through the loop. Here's the sort of thing I'm talking about:

class StoppableIterator(object):
    def __init__(self, iterable):
        self._iter = iter(iterable)
        self._stop = False
    def __iter__(self):
        return self
    def stop(self):
        self._stop = True
    def next(self):
        if self._stop:
            raise StopIteration
        return next(self._iter)

Usage:

si = StoppableIterator([2, 3, 5, 7, 11, 13])
for i in si:
    for j in xrange(i):
         print i, j
         if j == 7:
             si.stop()   # will break out of outer loop next iteration
             break       # breaks out of inner loop
kindall
  • 178,883
  • 35
  • 278
  • 309
  • Maybe there is some way we can tinker with StopIteration for it to be caught by necessary for-loop? I think it may be possible because if the iterator raises StopIteration, for-loop except section has to determine from where that StopIteration comes from and propagate it if it's not for it, but for some upper loops. So, by editing some parameters of StopIteration object we can make it be caught by specific for-loop. – ovgolovin Aug 03 '11 at 00:18
  • No, it doesn't have to "determine where it came from" -- it simply puts a `try/except` around the `next()` call, which gets the next value from the iterator. If catches the exception, it knows it came from the iterator. You could conceivably write an iterator to raise `StopException` the next time it's called after a special method is called, though. It would only be able to break out of the loop at the top, though. Better to just raise it wherever you need to and catch it yourself. – kindall Aug 03 '11 at 00:23
  • Oh. Now I see it. So when we write something: for i in iterable: #do_sth for-loop decorates the next() method of the iterator and catches the StopIteration. I don't undertand how can I write an iterator to raise StopException the next time it's called after a special method is called, just haven't managed to get the idea. – ovgolovin Aug 03 '11 at 00:33
  • Sorry, I can't vote you post up (don't have enough rep). You post was really helpful! – ovgolovin Aug 03 '11 at 01:17
  • I'd rename the `stop` method to `close`, because [generators have `.close` method](https://docs.python.org/2/whatsnew/2.5.html#pep-342-new-generator-features). – GingerPlusPlus Nov 23 '14 at 11:18