Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it safe to use next within a for loop in Python?

Tags:

python

Consider the following Python code:

b = [1,2,3,4,5,6,7]
a = iter(b)
for x in a :
    if (x % 2) == 0 :
        print(next(a))

Which will print 3, 5, and 7. Is the use of next on the variable being iterated on a reliable construct (you may assume that a StopIteration exception is not a concern or will be handled), or does the modification of the iterator being looped over inside the loop constitute a violation of some principle?

like image 744
Jack Aidley Avatar asked Dec 13 '18 14:12

Jack Aidley


People also ask

How do you go next to a for loop in Python?

You can use the continue statement if you need to skip the current iteration of a for or while loop and move onto the next iteration.

What does next () do in Python?

The next() function returns the next item in an iterator. You can add a default return value, to return if the iterable has reached to its end.

Can we use else after for loop in Python?

Python allows the else keyword to be used with the for and while loops too. The else block appears after the body of the loop. The statements in the else block will be executed after all iterations are completed. The program exits the loop only after the else block is executed.

Can you have two for loops Python?

Nested For LoopsLoops can be nested in Python, as they can with other programming languages. The program first encounters the outer loop, executing its first iteration. This first iteration triggers the inner, nested loop, which then runs to completion.


Video Answer


2 Answers

There's nothing wrong here protocol-wise or in theory that would stop you from writing such code. An exhausted iterator it will throw StopIteration on every subsequent call to it.__next__, so the for loop technically won't mind if you exhaust the iterator with a next/__next__ call in the loop body.

I advise against writing such code because the program will be very hard to reason about. If the scenario gets a little more complex than what you are showing here, at least I would have to go through some inputs with pen and paper and work out what's happening.

In fact, your code snippet possibly does not even behave like you think it behaves, assuming you want to print every number that is preceded by an even number.

>>> b = [1, 2, 4, 7, 8]                                              
>>> a = iter(b)                                                      
>>> for x in a: 
...:    if x%2 == 0: 
...:        print(next(a, 'stop'))                                   
4
stop

Why is 7 skipped although it's preceded by the even number 4?

>>>> a = iter(b)                                                      
>>>> for x in a: 
...:     print('for loop assigned x={}'.format(x)) 
...:     if x%2 == 0: 
...:         nxt = next(a, 'stop') 
...:         print('if popped nxt={} from iterator'.format(nxt)) 
...:         print(nxt)
...:                                               
for loop assigned x=1
for loop assigned x=2
if popped nxt=4 from iterator
4
for loop assigned x=7
for loop assigned x=8
if popped nxt=stop from iterator
stop

Turns out x = 4 is never assigned by the for loop because the explicit next call popped that element from the iterator before the for loop had a chance to look at the iterator again.

That's something I'd hate to work out the details of when reading code.


If you want to iterate over an iterable (including iterators) in "(element, next_element)" pairs, use the pairwise recipe from the itertools documentation.

from itertools import tee                                         

def pairwise(iterable):
    "s -> (s0,s1), (s1,s2), (s2, s3), ..." 
    a, b = tee(iterable) 
    next(b, None) 
    return zip(a, b) 

Demo:

>>> b = [1,2,3,4,5,6,7]                                               
>>> a = iter(b)                                                       
>>>                                                                   
>>> for x, nxt in pairwise(a): # pairwise(b) also works 
...:    print(x, nxt)                                                                      
1 2
2 3
3 4
4 5
5 6
6 7

In general, itertools together with its recipes provides many powerful abstractions for writing readable iteration-related code. Even more useful helpers can be found in the more_itertools module, including an implementation of pairwise.

like image 64
timgeb Avatar answered Oct 28 '22 09:10

timgeb


It depends what you mean by 'safe', as others have commented, it is okay, but you can imagine some contrived situations that might catch you out, for example consider this code snippet:

b = [1,2,3,4,5,6,7]
a = iter(b)
def yield_stuff():
    for item in a:
        print(item)
        print(next(a))
    yield 1

list(yield_stuff())

On Python <= 3.6 it runs and outputs:

1
2
3
4
5
6
7

But on Python 3.7 it raises RuntimeError: generator raised StopIteration. Of course this is expected if you read PEP 479 and if you're thinking about handling StopIteration anyway you might never encounter it, but I guess the use cases for calling next() inside a for loop are rare and there are normally clearer ways of re-factoring the code.

like image 4
Chris_Rands Avatar answered Oct 28 '22 08:10

Chris_Rands