Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Итераторы Python: Списки

На предыдущем уроке мы рассмотрели цикл for, и в тексте встретился термин "итерирование". И если в других языках это слово могут применять к любым циклам, то в Python у этого слова есть и другое значение: итерирование — это взаимодействие с неким объектом, поддерживающим протокол итерации.

Для начала разберём, что же такое протокол в контексте Пайтона. Протоколом называют набор определенных действий над объектом. И если некий объект "А" позволяет совершать над собой действия, описанные неким протоколом "Б", то говорят: "объект А реализует протокол Б" или "объект А поддерживает протокол Б". В последующих курсах вы узнаете, что различных протоколов в Python — множество. Даже многие синтаксические конструкции языка работают для самых разных объектов сходным образом именно потому, что объекты реализуют специальные протоколы. Так мы можем в шаблон подставлять не только строки, но и значения других типов, потому что эти типы реализуют протокол приведения к строке! В Python протоколы встречаются на каждом шагу.

Протокол итерации

Протокол итерации — один из самых важных протоколов в Python. Ведь именно он позволяет циклу for работать с самыми разными коллекциями единообразно. В чём же заключается этот протокол? Протокол требует от объекта быть итерируемым (iterable), т.е. иметь специальный метод __iter__ (да, в Python не только файлы принято называть в таком стиле). Если у iterable-объекта вызвать метод __iter__, то метод должен вернуть новый специальный объект — так называемый итератор (iterator). А итератор, в свою очередь, должен иметь метод __next__.

Звучит сложно, но давайте рассмотрим живой пример — итерирование списка. Список — итерируемый, поэтому нам подходит. Итак, создадим список и итератор для него:

l = [1,2,3,5,8,11]
i = iter(l)
print(i)  # => <list_iterator object at 0x7f517843a240>

Я вызвал для списка функцию iter, но на самом деле эта функция просто вызывает у списка соответствующий метод __iter__. Это сделано для удобства чтения кода, ведь читать имена вроде __foo__ не очень удобно. Некоторые другие функции делают что-то подобное, например функция len. Большинство же специальных методов с похожими именами вызывается внутри каких-то языковых конструкций и не предназначено для вызова напрямую.

Теперь у нас есть итератор i, попробуем повызывать у него метод __next__ как напрямую, так и с помощью более удобной функции next:

i.__next__()  # 1
i.__next__()  # 2
next(i)  # 3
next(i)  # 5

Как мы видим, при каждом вызове метод возвращает очередной элемент исходного списка. А между вызовами он помнит свою позицию в списке. Т.о. итератор выполняет роль "курсора" в вашем редакторе текста: если нажимать стрелки, то курсор перемещается и указывает на новое место в тексте. Только итератор — это курсор, умеющий перемещаться только в одну сторону.

Но что же произойдёт, когда элементы в списке кончатся? Проверим:

next(i)  # 8
next(i)  # 11
next(i)
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# StopIteration

Когда итератор достиг конца исходного списка, последующий вызов next привёл к специальной ошибке StopIteration. Только в данном случае это не ошибка, ведь всё когда-нибудь заканчивается! StopIteration — это исключение (exception). Об исключениях мы поговорим позже, а пока нужно лишь знать, что те средства языка, которые работают на основе протокола итерации, знают, как реагировать на это конкретное исключение. Например, цикл for "молча" завершает работу :)

Теперь вы уже можете представить, как на самом деле работает цикл for. Он получает у iterable объекта новый итератор, а затем вызывает у итератора метод __next__ до тех пор, пока не будет выброшено исключение StopIteration. Интересно, не правда ли? Но дальше будет ещё интереснее!

Цикл for и итераторы

Что же будет, если сначала получить итератор, а потом передать его циклу for? Такое возможно, ведь цикл for достаточно умён — он понимает, что можно сразу начать вызывать __next__!

Давайте напишем функцию, ищущую в цикле первую строку, длина которой больше пяти символов:

def search_long_string(source):
    for item in source:
        if len(item) >= 5:
            return item

А теперь создадим список, содержащий несколько подходящих строк, и запустим функцию для этого списка пару раз:

animals = ['cat', 'mole', 'tiger', 'lion', 'camel']
search_long_string(animals)  # 'tiger'
search_long_string(animals)  # 'tiger'

Функция оба раза вернула одну и ту же строку, ведь мы передали в неё iterable, а значит цикл for создавал каждый раз новый итератор.

Но давайте же создадим итератор сами и передадим в функцию уже его:

animals = ['cat', 'mole', 'tiger', 'lion', 'camel']
cursor = iter(animals)
search_long_string(cursor)  # 'tiger'
search_long_string(cursor)  # 'camel'
search_long_string(cursor)
search_long_string(cursor)

Уже интереснее! Итератор запомнил состояние между вызовами функций, и мы нашли оба длинных слова. Последующие вызовы функции не вернули ничего (вернули None), потому что итератор дошёл до конца (и запомнил это).

А ведь итераторов для одного и того же списка можно создать несколько! И каждый будет помнить свою позицию! Работая с кодом на Python вы непременно увидите, и не раз, интересные применения протокола итерации. А поэкспериментировать прямо в REPL вы можете уже сейчас!

Генераторы

В Python не только коллекции являются iterable. Ещё существуют так называемые генераторы (generators). Что же такое генератор? Генератор — это iterable, элементы которого не хранятся в нём, но создаются по мере необходимости. Для примера возьмём генератор range. Вот как он работает:

numbers = range(3, 11, 2)
for n in numbers:
    print(n)

# => 3
# => 5
# => 7
# => 9
list(numbers)  # [3, 5, 7, 9]

Здесь range генерирует последовательность чисел от 3 до (но не включая) 11 с шагом 2. Шаг и начальное значения можно опускать, тогда счёт будет производиться от нуля и с шагом в единицу. Цикл for итерирует числа. Затем я использую функцию list, чтобы получить список — эта функция может принять в качестве единственного аргумента iterable или iterator, элементы которого сложит во вновь созданный список.

Функция list накапливает значения в список, а tuple — в кортеж.

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

Существуют и не перезапускаемые генераторы. Эти при вызове метода __iter__ всегда возвращают один и тот же итератор. Поэтому по значениям такого генератора можно пройтись только один раз! Примером такого генератора является enumerate, который мы рассматривали на прошлом уроке. Давайте ещё раз взглянем на него:

l = enumerate("asdf")
list(l)  # [(0, 'a'), (1, 's'), (2, 'd'), (3, 'f')]
list(l)  # []

Вторая попытка проитерировать объект в переменной l ничего не даёт, т.к. генератор уже отработал один проход.

А вот ещё один встроенный генератор — zip. Этот генератор принимает на входе несколько iterable или iterators и поэлементно группирует в кортежи. Демонстрация:

keys = ["foo", "bar", "baz"]
values = [1, 2, 3, 4]
for k, v in zip(keys, values):
    print(k, "=", v)
# => foo = 1
# => bar = 2
# => baz = 3

z = zip(range(10), "hello", [True, False])
list(z)  # [(0, 'h', True), (1, 'e', False)]
list(z)  # []

Пример демонстрирует два момента:

  1. zip — не перезапускаемый,
  2. zip — перестаёт генерировать кортежи, как только заканчиваются элементы в любом из источников.

Генераторы и ленивые вычисления

Большая часть языков программирования выполняет код в том порядке, в котором элементы кода написаны. Инструкции выполняются сверху вниз, выражения вычисляются после того, как будут вычислены их составляющие, функции вызываются после того, как будут вычислены их аргументы. Такая модель исполнения называется энергичной (eager).

Существует и ленивая (lazy) модель вычисления. В рамках этой модели вычисления производятся только тогда, когда их результат становится действительно нужен. Т.к. в любой программе при разных входных данных могут быть не нужны отдельные вычисления, то ленивая модель вычисления может дать определённые преимущества: то, что не нужно — не будет вычислено. Т.о. ленивость можно рассматривать как своего рода оптимизацию.

Python, как язык с энергичной моделью вычисления, практически всегда и всё вычисляет сразу. Однако отдельные элементы ленивости присутствуют и в Пайтоне. Генераторы — один из таких элементов. Генераторы производят элементы только по мере необходимости. И даже целые конструкции, собранные из генераторов — эдакие конвейеры, собирающие составные значения — производят сборку по одному изделию за раз!

Так составной генератор zip(range(100000000), "abc") не генерирует все сто миллионов чисел, ведь строка "abc" слишком коротка, чтобы образовать столько пар. Но даже и этих пар не будет, если результат вычисления этого выражения не будет проитерирован! Так ленивость позволяет экономить память при обработке больших потоков данных — нам не нужно загружать все данные целиком, достаточно загружать и обрабатывать их небольшими порциями.

Ссылки

  • itertools — очень полезный модуль стандартной библиотеки. Содержит множество функций для создания итераторов и дальнейшей работы с ними.

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.

Об обучении на Хекслете

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
900
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно.

  • 130 курсов, 2000+ часов теории
  • 900 практических заданий в браузере
  • 360 000 студентов
Даю согласие на обработку персональных данных, соглашаюсь с «Политикой конфиденциальности» и «Условиями оказания услуг»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Python-разработчик
Профессия
Разработка веб-приложений на Django
1 июня 10 месяцев

Используйте Хекслет по максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Даю согласие на обработку персональных данных, соглашаюсь с «Политикой конфиденциальности» и «Условиями оказания услуг»