Вопрос-Ответ

Why can't I iterate twice over the same iterator? How can I "reset" the iterator or reuse the data?

Почему я не могу выполнить итерацию дважды по одному и тому же итератору? Как я могу "сбросить" итератор или повторно использовать данные?

Рассмотрим код:

def test(data):
for row in data:
print("first loop")
for row in data:
print("second loop")

Когда data есть итератор, например, итератор списка или выражение генератора *, это не работает:

>>> test(iter([1, 2]))
first loop
first loop
>>> test((_ for _ in [1, 2]))
first loop
first loop

Это печатается first loop несколько раз, поскольку data непустое значение. Однако оно не печатается second loop. Почему повторное выполнение data работает в первый раз, но не во второй? Как я могу заставить это работать во второй раз?

Помимо for циклов, похоже, та же проблема возникает при любом типе итерации: понимании списка / набора / dict, передаче итератора в list(), sum() или reduce() и т.д.

С другой стороны, если data есть другой вид итерируемости, такой как list или a range (которые оба являются последовательностями), оба цикла выполняются как ожидалось:

>>> test([1, 2])
first loop
first loop
second loop
second loop
>>> test(range(2))
first loop
first loop
second loop
second loop

* Еще примеры:


Для объяснения общей теории и терминологии см. Что такое iterator, iterable и повторение?.

Чтобы определить, является ли ввод итератором или итерацией "многократного использования", см. раздел Убедитесь, что аргумент может быть повторен дважды.

Переведено автоматически
Ответ 1

Итератор может быть использован только один раз. Например:

lst = [1, 2, 3]
it = iter(lst)

next(it)
# => 1
next(it)
# => 2
next(it)
# => 3
next(it)
# => StopIteration

Когда итератор передается в for цикл вместо этого, этот последний StopIteration приведет к завершению работы в первый раз. Попытка использовать тот же итератор в другом цикле for немедленно вызовет StopIteration повторение, потому что итератор уже был использован.

Простой способ обойти это - сохранить все элементы в списке, который можно просматривать столько раз, сколько необходимо. Например:

data = list(data)

Однако, если итератор будет выполнять итерации по многим элементам, лучше создать независимые итераторы, используя tee():

import itertools
it1, it2 = itertools.tee(data, 2) # create as many as needed

Теперь каждую итерацию можно выполнять по очереди:

for e in it1:
print("first loop")

for e in it2:
print("second loop")
Ответ 2

Итераторы (например, из вызова iter, из выражений генератора или из функций генератора, которые yield) отслеживают состояние и могут быть использованы только один раз.

Это объясняется в ответе Оскара Лопеса, однако рекомендация этого ответа использовать itertools.tee(data) вместо list(data) из соображений производительности вводит в заблуждение. В большинстве случаев, когда вы хотите выполнить итерацию по всему data, а затем выполнить итерацию по всему этому снова, tee занимает больше времени и использует больше памяти, чем простое преобразование всего итератора в список, а затем повторное выполнение по нему дважды. Согласно документации:


Этот инструмент itertool может потребовать значительного дополнительного хранилища (в зависимости от того, сколько временных данных необходимо сохранить). В общем, если один итератор использует большую часть или все данные до запуска другого итератора, быстрее использовать list() вместо tee().


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

Ответ 3

Как только итератор будет исчерпан, он больше ничего не даст.

>>> it = iter([3, 1, 2])
>>> for x in it: print(x)
...
3
1
2
>>> for x in it: print(x)
...
>>>
Ответ 4

Как мне дважды выполнить цикл по итератору?

Обычно это невозможно. (Объяснено позже.) Вместо этого выполните одно из следующих действий:


  • Собрать итератор во что-то, что можно повторять несколько раз.


    items = list(iterator)

    for item in items:
    ...

    Недостаток: это требует затрат памяти.



  • Создайте новый итератор. Обычно создание нового итератора занимает всего микросекунду.


    for item in create_iterator():
    ...

    for item in create_iterator():
    ...

    Недостаток: сама итерация может быть дорогостоящей (например, чтение с диска или сети).



  • Сбросить "итератор". Например, с файловыми итераторами:


    with open(...) as f:
    for item in f:
    ...

    f.seek(0)

    for item in f:
    ...

    Недостаток: большинство итераторов невозможно "сбросить".




Философия Iterator

Обычно, хотя и не технически1:


  • Iterable: Зацикливаемый объект, представляющий данные. Примеры: list, tuple, str.

  • Итератор: указатель на некоторый элемент итерируемого объекта.

Если бы мы определили итератор последовательности, это могло бы выглядеть примерно так:

class SequenceIterator:
index: int
items: Sequence # Sequences can be randomly indexed via items[index].

def __next__(self):
"""Increment index, and return the latest item."""

Здесь важно то, что обычно итератор не хранит внутри себя никаких реальных данных.

Итераторы обычно моделируют временный "поток" данных. Этот источник данных используется в процессе итерации. Это хороший намек на то, почему нельзя выполнять цикл по произвольному источнику данных более одного раза. Для этого нам нужно открыть новый временный поток данных (т. Е. Создать новый итератор).

Исчерпывающий Iterator

Что происходит, когда мы извлекаем элементы из итератора, начиная с текущего элемента итератора и продолжая до тех пор, пока он полностью не будет исчерпан? Это то, что делает for цикл:

iterable = "ABC"
iterator = iter(iterable)

for item in iterator:
print(item)

Давайте поддержим эту функциональность в SequenceIterator, сообщив for циклу, как извлечь next элемент:

class SequenceIterator:
def __next__(self):
item = self.items[self.index]
self.index += 1
return item

Подождите. Что, если index пройдет мимо последнего элемента items? Мы должны создать для этого безопасное исключение:

class SequenceIterator:
def __next__(self):
try:
item = self.items[self.index]
except IndexError:
raise StopIteration # Safely says, "no more items in iterator!"
self.index += 1
return item

Теперь цикл for знает, когда прекратить извлечение элементов из итератора.

Что произойдет, если мы сейчас попытаемся повторить цикл по итератору снова?

iterable = "ABC"
iterator = iter(iterable)

# iterator.index == 0

for item in iterator:
print(item)

# iterator.index == 3

for item in iterator:
print(item)

# iterator.index == 3

Поскольку второй цикл начинается с текущего iterator.index, который равен 3, ему больше нечего печатать, и поэтому iterator.__next__ вызывает StopIteration исключение, в результате чего цикл немедленно завершается.


1 Технически:


  • Iterable: объект, который возвращает итератор при __iter__ вызове на нем.

  • Итератор: объект, который можно многократно вызывать __next__ в цикле для извлечения элементов. Кроме того, вызов __iter__ для него должен возвращать егоself.

Подробнее здесь.

2023-07-04 09:23 python