Как создать итератор в Python — Полный обзор генераторов

Как создать итератор в Python

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

Содержание статьи

Что такое итератор?

Сначала давайте быстро разберемся, что такое итератор. Для более подробного объяснения посмотрите видео «Итератор и итерируемые объекты. Функции iter() и next()» от автора selfedu.

Есть вопросы по Python?

На нашем форуме вы можете задать любой вопрос и получить ответ от всего нашего сообщества!

Telegram Чат & Канал

Вступите в наш дружный чат по Python и начните общение с единомышленниками! Станьте частью большого сообщества!

Паблик VK

Одно из самых больших сообществ по Python в социальной сети ВК. Видео уроки и книги для вас!

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

Итератор — это объект, который выполняет фактическую итерацию.

Вы можете создать итератор из любого итерабельного объекта, вызвав встроенную функцию iter():

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

Есть еще одно правило об итераторах, которое делает все намного интереснее: итераторы также являются итераторабельными объектами, а их итератор — это они сами.

Зачем нужно создать итератор?

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

Использование итератора вместо списка, множества или другой итерирабельной структуры данных иногда позволяет экономить память. Например, мы можем использовать itertools.repeat для создания итератора, который предоставит нам 100 миллионов четверок (4):

На моем компьютере этот итератор занимает 56 байт памяти:

Такой же список из 100 миллионов четверок созданный более примитивным способом занимает 762.94 Мб:

Хотя итераторы могут экономить память, они также могут экономить время. Например, если вы хотите вывести только первую строку из 10-гигабайтного файла с логами, вы можете сделать следующее:

Файловые объекты в Python реализованы как итераторы. При итерации по файлу данные считываются в память по одной строке за раз. Если бы вместо этого мы использовали метод readlines для хранения всех строк в памяти, мы могли бы исчерпать всю системную память и убить процесс.

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

Кроме того, у итераторов есть возможности, которых нет у других итерабельных объектов. Например, их «лень» можно использовать для создания итерабельных объектов неизвестной длины. На самом деле, можно даже создавать бесконечно длинные итераторы.

Например, метод itertools.count создаст нам итератор, который будет выдавать каждое следующее число от 0 до «бесконечности» в зависимости когда вы завершите цикл:

Результат:

Метод itertools.count по сути является бесконечно длинным итерабельным объектом. И он реализован как итератор.

Объектно-ориентированный итератор

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

Давайте создадим свои собственные итераторы. Для начала мы «изобретем» заново объект итератора itertools.count.

Вот итератор, реализованный с помощью класса:

В этом классе есть конструктор, который инициализирует текущее число итератора на 0 (или то, что было передано в качестве начала из аргумента start). То, что превращает этот класс в итератора, это наличие методов __iter__ и __next__.

Когда объект передается встроенной функции str, вызывается метод __str__. Когда объект передается встроенной функции len, вызывается ее метод __len__.

  • Передав наш объект в функцию iter это приведет к попытке вызвать его метод __iter__.
  • Передав наш объект в функцию next это приведет к попытке вызвать его метод __next__.

Предполагается, что функция iter возвращает итератор. По этой причине метод __iter__ должен возвращать итератор. Но наш объект сам по себе является итератором, поэтому он должен возвращать самого себя. Объект Count возвращает self из своего метода __iter__, так как он является собственным итератором.

Функция next должна возвращать следующий элемент в итераторе или вызывать исключение StopIteration, если элементов больше нет. Мы возвращаем текущее число и увеличиваем его на единицу, чтобы оно было больше во время следующего вызова метода __next__.

Мы можем вручную перебирать объект Count следующим образом:

Мы также можем перебирать объект Count, используя цикл for, как и любой другой итерабельный объект:

Результат:

Такой объектно-ориентированный подход к созданию итератора — это здорово, но это не типичный способ, которым Python-программисты создают итераторы. Обычно, когда нам нужен итератор, мы создаем генератор.

Генераторы: простой способ создания итератора

Самый простой способ создания собственных итераторов в Python — это создание генератора.

В Python есть два способа создания генераторов.

Дан список чисел:

Мы можем сделать генератор, который будет лениво выдавать все квадраты этих чисел следующим образом:

Или мы можем сделать такой же генератор следующим образом:

Первый подход называется функцией-генератором, а второй — выражением-генератором.

Оба этих объекта-генератора работают одинаково. Они оба имеют тип generator и оба являются итераторами, которые предоставляют квадраты чисел из нашего списка чисел.

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

Слово «генератор» в Python используется в разных смыслах:

  • Генератор, также называемый объектом-генератором, — это итератор, тип которого — generator;
  • Функция-генератор — это специальный синтаксис, который позволяет нам создать функцию, возвращающую объект-генератор при вызове;
  • Выражение-генератор — это синтаксис, напоминающий представление списков (list comprehension), которое позволяет создавать объект-генератор в одну линию кода.

Убрав эту терминологию, давайте рассмотрим каждую из этих вещей по отдельности. Сначала мы рассмотрим функции-генераторы.

Функции-генераторы

Функции-генераторы отличаются от обычных функций тем, что в них есть один или несколько операторов yield.

Обычно при вызове функции выполняется ее код:

Но если в теле функции есть оператор yield, то это уже не обычная функция. Теперь это функция-генератор, то есть при вызове она возвращает объект-генератор. Этот объект-генератор может выполняться в цикле до тех пор, пока не будет выполнен оператор yield:

Одно только присутствие оператора yield превращает функцию в функцию-генератор. Если вы видите функцию и в ней есть оператор yield, вы работаете с чем-то иным нежели с обычной функцией. Это немного странно, но именно так работают функции-генераторы.

Хорошо, давайте рассмотрим реальный пример функции-генератора. Мы создадим функцию-генератор, которая будет делать то же самое, что и класс-итератор Count, который мы создали ранее.

Подобно классу-итератору Count, мы можем вручную перебирать генератор, полученный в результате вызова функции count:

И мы можем перебирать этот объект генератора с помощью цикла for, как и раньше:

Результат:

Согласитесь, что данная функция значительно короче и понятнее, чем класс Count, который мы создали ранее.

Выражения-генераторы

Выражения-генераторы — это синтаксис, похожий на синтаксис представления списка (list comprehension), который позволяет нам создать объект-генератор.

Допустим, у нас есть представление-списка, который фильтрует пустые строки из файла и удаляет переход на новую строку в конце \n:

Мы можем создать генератор вместо списка, превратив квадратные скобки в круглые скобки:

Точно так же, как представление списков (list comprehension) вернуло бы нам список, выражение-генератор вернет нам объект-генератор:

Выражения-генераторы используют более короткий синтаксис кода по сравнению с функциями-генераторами. Однако они не такие мощные.

Вы можете написать свою функцию-генератор в такой форме:

Затем вы можете заменить тело функции на выражение-генератор:

Если вы не можете написать свою функцию-генератор в такой форме, то вы не сможете создать выражение-генератор для её замены.

Обратите внимание, что мы изменили используемый пример, потому что мы не можем использовать выражение-генератор для предыдущего примера, который реализует itertools.count который по сути является вечным циклом.

Выражения-генераторы или функции-генераторы?

Выражения-генераторы можно рассматривать как представление-списков (list comprehensions) в мире генераторов.

Если в не знакомы со представлениыем-списков, рекомендую прочитать об этом статью. В этой статье описывается путь от цикла for к list comprehensions.

Также можно скопировать код из функции-генератора и вставить в обычную функцию которая возвращает выражение-генератор:

Итераторы в Python

Выражения-генераторы являются функциями-генераторами так же, как представление-списков являются простым циклом for с добавлением и условием.

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

Нед Батчелдер фактически предложил, чтобы мы все начали называть выражения-генераторы (generator expressions) как представление-генераторов (generator comprehensions), и я склонен согласиться, что это было бы более понятным названием.

Лучший способ создания итератора

Чтобы создать итератор, можно создать класс-итератор, функцию-генератор или выражение-генератор. Но какой способ лучше?

Выражения-генераторы очень лаконичны, но они не такие гибкие, как функции-генераторы. Функции-генераторы гибкие, но если вам нужно добавить дополнительные методы или атрибуты к объекту-итератору, то, скорее всего, придется перейти на использование класса-итератора.

Я бы рекомендовал смотреть в сторону к выражениям-генераторам так же и представление-списков (list comprehensions). Если вы выполняете простую операцию вывода или фильтрации, выражение-генератор — отличное решение. Если вы делаете что-то более сложное, вам, скорее всего, понадобится функция-генератор.

Я бы рекомендовал использовать функции-генераторы так же, как использование цикла for для добавления данных в список. Везде, где требуется метод append, вы зачастую увидите оператор yield вместо него.

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

Генераторы могут помочь при создании итераторов

Вы можете встретить классы-итераторы, но редко попадается хорошая возможность написать свой собственный.

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

Например, вот итератор, который предоставляет координаты x-y:

Обратите внимание, что при вызове класса Point создается итерабельный объект (а не итератор). Это означает, что метод __iter__ должен возвращать итератор. Самый простой способ создать итератор — это создать функцию-генератор, что мы и сделали.

Мы вставили yield в метод __iter__, чтобы превратить его в функцию-генератор, и теперь класс Point можно перебирать, как и любой другой итерабельный объект.

Функции-генераторы естественным образом подходят для создания методов __iter__ в итерабельных классах.

Генераторы — это способ создания итераторов

Словари — типичный способ создания карт в Python. Функции — типичный способ создания вызываемого объекта в Python. Аналогично, генераторы — это типичный способ создания итератора в Python.

Поэтому, когда вы думаете: «Было бы неплохо реализовать итерабельный объект, который бы лениво вычислял что-то по мере выполнения цикла», подумайте об итераторах.

А когда вы думаете о том, как создать свой собственный итератор, вспомните о функциях-генераторах и выражениях-генераторах.