Итераторы

Списки, кортежи, множества удобно обрабатывать поэлементно. Представим, что вы хотите проверить задание у детей в классе. Логично что дети будут находиться в каком-то порядке, например сидеть за партами и вы будете подходить к следующему и просить открыть тетрадь. Или будете идти по журналу и опрашивать детей в алфавитном порядке. А на уроке физкультуры часто просят детей выстроиться по росту. И когда вы пройдете по каждому элементу множества (детей) то значит, что все дети опрошены. Вам не надо сразу вызывать весь класс к доске или просить их всех одновременно сказать свою фамилию. Идти шаг за шагом вполне естественно. И в Python'е есть специальный протокол работы с последовательностями, который называется работа с итераторами. Вы можете превратить любую последовательность в итератор с помощью функции iter. Этот объект будет выдавать вам по одному значению за раз пока не закончится вся последовательность:

>>> l = [1, 2, 3,]
>>> i = iter(l)
>>> type(i)
<class 'list_iterator'>

Теперь для того чтобы получить следующий элемент есть специальная функция next:

>>> l = [1, 2, 3,]
>>> i = iter(l)
>>> next(i)
1
>>> next(i)
2
>>> next(i)
3
>>> next(i)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

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

Давайте попробуем создать итератор еще раз и посмотрим как применяется протокол работы с итераторами в конструкторах известных типов. Например отдадим его конструктору списков:

>>> new_iter = iter([3, 4, 5])
>>> list(new_iter)
[3, 4, 5]

Или конструктору кортежей:

>>> new_iter = iter([3, 4, 5])
>>> tuple(new_iter)
(3, 4, 5)

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

Но в python-е не обязательно каждый раз держать все элементы в памяти. Вместо того чтобы загрузить весь большой файл в память вы можете обрабатывать его построчно по мере чтения. Или не обязательно ждать пока по сети скачается все видео, если можно начать его проигрывать по мере поступления новых данных.

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

Немного больше магии ленивой оптимизации итераторов

Когда мы вызываем функцию all() то она по идее должна идти по всем элементам последовательности и проверять являются ли все значения True. Но надо ли в действительности проверять остальные значения если хотя бы одно False? Нет, конечно же на самом деле эта функция проверяет последовательность ровно до тех пор пока не найдет первое значение False и тут же вернет результат. Давайте проверим на практике. Если мы создадим итератор для последовательности содержащей False, то значит, что после вызова all и остановки итератор все еще будет содержать неиспользованные элементы:

>>> iter_seq = iter([1, 2, 0, 3, 4])
>>> all(iter_seq)
False
>>> next(iter_seq)
3
>>> next(iter_seq)
4
>>> next(iter_seq)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Как видите после элемента со значением 0 итератор прекратил свою работу.

Когда вы обращаетесь к методам словаря keys() или values() происходит похожий процесс. Вместо того чтобы сделать список который занимает место в памяти создается специальный "скользящий" объект который просто знает какой следующий элемент надо вернуть. Но если во время обработки итератора исходная последовательность изменится, то это может вызвать неожиданные последствия или ошибку. Со списками разобраться проще всего:

>>> seq = [1, 2, 3]
>>> new = iter(seq)
>>> next(new)
1
>>> seq.pop()
3
>>> next(new)
2
>>> next(new)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Как видите когда мы изменили исходную последовательность и удалили один элемент, то итератор не смог его уже вернуть. Но не путайте переменную и объект который расположен в памяти. Мы уже несколько раз к этому обращались, давайте я напомню вам это еще раз. В Python есть изменяемые (мутабельные) переменные и есть идентификаторы которые на них указывают. Когда мы идентификатору присваиваем абсолютно новое значение, то переменная начинает указывать на новую область памяти, но объект итератор указывает все еще на старую.

>>> seq = [1, 2, 3]
>>> new = iter(seq)
>>> seq = 'abc'
>>> next(new)
1

Или даже так:

>>> seq = [1, 2, 3]
>>> new = iter(seq)
>>> del seq
>>> seq
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'seq' is not defined
>>> next(new)
1

Когда мы будем учиться работать с файлами то умение обрабатывать входящие данные построчно сильно пригодится, ведь файлы могут занимать очень много места.