Безопасно ли использовать следующий цикл for в Python?

Рассмотрим следующий код Python:

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

Который будет печатать 3, 5 и 7. Является ли использование next для итерируемой переменной надежной конструкции (вы можете предположить, что исключение StopIteration не является проблемой или будет обработано), или выполняет модификацию итератора зацикленные внутри цикла представляют собой нарушение какого-то принципа?

Ответ 1

Здесь нет ничего плохого в протоколе или в теории, что помешало бы вам писать такой код. Измученный итератора it будет бросать StopIteration на каждом последующем вызове it.__next__, поэтому for цикла технически не будет возражать, если вы исчерпывают итератор с next/__next__ вызова в теле цикла.

Я не советую писать такой код, потому что о программе будет очень сложно рассуждать. Если сценарий становится немного более сложным, чем то, что вы показываете здесь, по крайней мере, мне нужно было бы просмотреть некоторые входные данные ручкой и бумагой и выяснить, что происходит.

Фактически, ваш фрагмент кода, возможно, даже не ведет себя так, как вы думаете, он ведет себя, предполагая, что вы хотите напечатать каждое число, которому предшествует четное число.

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

Почему пропускается 7 хотя ему предшествует четное число 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

Оказывается, x = 4 никогда не назначается циклом for потому что явный next вызов вытолкнул этот элемент из итератора до того, for цикл for снова смог взглянуть на итератор.

Это то, что я не хотел бы прорабатывать детали при чтении кода.


Если вы хотите перебрать итератора (включая итераторы) в " (element, next_element) " пар, использовать pairwise рецепт от itertools документации.

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) 

Демо-версия:

>>> 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

В общем, itertools вместе со своими рецептами предоставляет множество мощных абстракций для написания читаемого кода, связанного с итерациями. Еще больше полезных помощников можно найти в модуле more_itertools, включая реализацию pairwise.

Ответ 2

Это зависит от того, что вы подразумеваете под "безопасным", как прокомментировали другие, это нормально, но вы можете себе представить некоторые надуманные ситуации, которые могут вас поймать, например, рассмотрите следующий фрагмент кода:

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())

На Python <= 3.6 он работает и выдает:

1
2
3
4
5
6
7

Но на Python 3.7 он вызывает RuntimeError: generator raised StopIteration. Конечно, это ожидаемо, если вы прочитаете PEP 479, и если вы в любом случае думаете о том, как обработать StopIteration вы никогда не столкнетесь с этим, но я предполагаю, что случаи использования для вызова next() внутри цикла for редки, и обычно существуют более ясные способы перефакторинг кода.

Ответ 3

Если вы измените свой код, чтобы увидеть, что происходит с итератором a:

b = [1,2,3,4,5,6,7]
a = iter(b)

for x in a :
    print 'x', x
    if (x % 2) == 0 :
        print 'a', next(a)

Вы увидите распечатку:

x 1
x 2
a 3
x 4
a 5
x 6
a 7

Это означает, что когда вы делаете next(a) вы продвигаете свой итератор вперед. Если вам нужно (или понадобится в будущем) сделать что-то еще с итератором a, у вас будут проблемы. Для полной безопасности используйте различные рецепты из модуля itertools. Например:

from itertools import tee, izip

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

b = [1,2,3,4,5,6,7]
a = iter(b)
c = pairwise(a)

for x, next_x in c:
    if x % 2 == 0:
        print next_x

Не то чтобы здесь у вас был полный контроль в любом месте цикла, как на текущем элементе итератора, так и на следующем.