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

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

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

Для начала разберем, что же такое протокол в контексте Python. Протоколом называют набор определенных действий над объектом.

Если некий объект А позволяет совершать над собой действия, описанные неким протоколом Б, то говорят: «объект А реализует протокол Б» или «объект А поддерживает протокол Б».

В последующих курсах вы узнаете, что различных протоколов в Python — множество.

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

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

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

Протокол итерации — один из самых важных протоколов в Python. Ведь именно он позволяет циклу for работать с самыми разными коллекциями единообразно.

В чем же заключается этот протокол? Протокол требует от объекта быть итерируемым — то есть иметь специальный метод __iter__.

Если у итерируемого объекта вызвать метод __iter__, то метод должен вернуть новый специальный объект — так называемый итератор. В свою очередь, итератор должен иметь метод __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 — это исключение. Об исключениях мы поговорим позже. А пока нужно лишь знать, что те средства языка, которые работают на основе протокола итерации, умеют реагировать на это конкретное исключение. Например, цикл for молча завершает работу.

Теперь вы уже можете представить, как на самом деле работает цикл for. Он получает у итерируемого объекта новый итератор. Затем вызывает у итератора метод __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, вы непременно увидите интересные применения протокола итерации.

Генераторы

В Python итерируемыми считаются не только коллекции. Еще существуют генераторы. Элементы генератора не хранятся в нем, но создаются по мере необходимости. Для примера возьмем генератор range. Вот как он работает:

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

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

Здесь range генерирует последовательность чисел от 3 до 10 с шагом 2. Шаг и начальное значения можно опускать, тогда счет будет производиться от нуля и с шагом в единицу.

Цикл for итерирует числа. Затем используем функцию list, чтобы получить список — эта функция может принять в качестве единственного аргумента итерируемый объект или итератор, элементы которого сложит во вновь созданный список.

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

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

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

Примером такого генератора является enumerate, который мы рассматривали на прошлом уроке. Давайте еще раз взглянем на него:

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

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

А вот еще один встроенный генератор — zip. Этот генератор принимает на входе несколько итерируемых объектов или итераторов и поэлементно группирует в кортежи:

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 — перестает генерировать кортежи, как только заканчиваются элементы в любом из источников

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

Большая часть языков программирования выполняет код в том порядке, в котором элементы кода написаны:

  • Инструкции выполняются сверху вниз
  • Выражения вычисляются после того, как будут вычислены их составляющие
  • Функции вызываются после того, как будут вычислены их аргументы

Такая модель исполнения называется энергичной.

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

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

Python — это язык с энергичной моделью вычисления, поэтому практически всегда и все вычисляет сразу. Однако отдельные элементы ленивости присутствуют и в Python.

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

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

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

Ссылки

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»