Python. Урок 15. Итераторы и генераторы
Генераторы и итераторы представляют собой инструменты, которые, как правило, используются для поточной обработки данных. В уроке рассмотрим концепцию итераторов в Python, научимся создавать свои итераторы и разберемся как работать с генераторами.
Итераторы в языке Python
Во многих современных языках программирования используют такие сущности как итераторы. Основное их назначение – это упрощение навигации по элементам объекта, который, как правило, представляет собой некоторую коллекцию (список, словарь и т.п.). Язык Python, в этом случае, не исключение и в нем тоже есть поддержка итераторов. Итератор представляет собой объект перечислитель, который для данного объекта выдает следующий элемент, либо бросает исключение, если элементов больше нет.
Приведем несколько примеров, которые помогут лучше понять эту концепцию. Для начала выведем элементы произвольного списка на экран.
Как уже было сказано, объекты, элементы которых можно перебирать в цикле for, содержат в себе объект итератор, для того, чтобы его получить необходимо использовать функцию iter(), а для извлечения следующего элемента из итератора – функцию next().
Как видно из приведенного выше примера вызов функции next(itr) каждый раз возвращает следующий элемент из списка, а когда эти элементы заканчиваются, генерируется исключение StopIteration.
Создание собственных итераторов
Если нужно обойти элементы внутри объекта вашего собственного класса, необходимо построить свой итератор. Создадим класс, объект которого будет итератором, выдающим определенное количество единиц, которое пользователь задает при создании объекта. Такой класс будет содержать конструктор, принимающий на вход количество единиц и метод __next__(), без него экземпляры данного класса не будут итераторами.
В нашем примере при четвертом вызове функции next() будет выброшено исключение StopIteration. Если мы хотим, чтобы с данным объектом можно было работать в цикле for, то в класс SimpleIterator нужно добавить метод __iter__(), который возвращает итератор, в данном случае этот метод должен возвращать self.
Генераторы
Генераторы позволяют значительно упростить работу по конструированию итераторов. В предыдущих примерах, для построения итератора и работы с ним, мы создавали отдельный класс. Генератор – это функция, которая будучи вызванной в функции next() возвращает следующий объект согласно алгоритму ее работы. Вместо ключевого слова return в генераторе используется yield. Проще всего работу генератор посмотреть на примере. Напишем функцию, которая генерирует необходимое нам количество единиц.
Данная функция будет работать точно также, как класс SimpleIterator из предыдущего примера.
Ключевым моментом для понимания работы генераторов является то, при вызове yield функция не прекращает свою работу, а “замораживается” до очередной итерации, запускаемой функцией next(). Если вы в своем генераторе, где-то используете ключевое слово return, то дойдя до этого места будет выброшено исключение StopIteration, а если после ключевого слова return поместить какую-либо информацию, то она будет добавлена к описанию StopIteration.
P.S.
Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas. На нашем сайте вы можете найти вводные уроки по этой теме. Все уроки по библиотеке Pandas собраны в книге “Pandas. Работа с данными”.
Итераторы в Python
Концепция итераторов никоим образом не специфична для Python. В самом общем виде это объект, который используется для перебора в цикле последовательности элементов. Однако разные языки программирования реализуют данную концепцию по-разному или не реализуют вовсе. В Python каждый цикл for использует итератор, в отличие от многих других языков. В данной статье мы поговорим про итераторы в Python. Кроме того, мы рассмотрим итерируемые объекты (англ. iterables) и т.н. nextables.
Итерируемые объекты
Обратите внимание, что итерируемый объект не обязательно является итератором. Поскольку на самом деле сам по себе он не выполняет итерацию. У вас может быть отдельный объект-итератор, который возвращается из итерируемого класса, а не класс, обрабатывающий свою собственную итерацию. Но об этом позже.
Итераторы
Перейдем к собственно итераторам, рабочей лошадке итерации (особенно в Python). Итераторы – это уровень абстракции, который инкапсулирует знания о том, как брать элементы из некоторой последовательности. Мы намеренно объясняем это в общем виде, поскольку «последовательность» может быть чем угодно, от списков и файлов до потоков данных из базы данных или удаленного сервиса. В итераторах замечательно то, что код, использующий итератор, даже не должен знать, какой источник используется. Вместо этого он может сосредоточиться только на одном, а именно: «Что мне делать с каждым элементом?».
Марк Лутц «Изучаем Python»
Скачивайте книгу у нас в телеграм
Итерация без итератора
Чтобы лучше понять преимущества итераторов, давайте кратко рассмотрим итерацию без итераторов. Примером итерации без итератора является классический цикл for в стиле C. Этот стиль существует не только в C, но и, например, в C++, go и JavaScript.
Пример того, как это выглядит в JavaScript:
Здесь мы видим, что данный тип цикла for должен работать как с извлечением, так и с действиями для каждого элемента.
Все циклы for в Python используют итераторы
Сначала давайте посмотрим на Python-эквивалент предыдущего примера, наиболее близкий к нему синтаксически:
Если вы внимательно посмотрите на пример на JavaScript, вы увидите, что мы сообщаем циклу, когда нужно завершить ( i ), а также — как инкременировать ( i++ ). Итак, чтобы приблизить код Python к такому уровню абстракции, нам нужно написать что-то вроде этого:
Протокол итератора в Python
Отметим, nextable – это не часто используемый термин, потому что его можно запросто превратить в итератор. Как видите, метод __iter__ для итераторов легко реализовать. Фактически, в определении итератора явно указано, что должен делать метод:
Теперь давайте превратим это в итератор, сделав «некстабельным». Метод __next__ должен возвращать следующий объект в последовательности. Он также должен вызывать StopIteration при достижении конца последовательности (т.н. «исчерпание итератора»). То есть, в нашем случае — когда мы дошли до конца алфавита.
Хорошо, теперь давайте посмотрим на код нашего класса, а затем мы объясним, как он работает:
Теперь давайте попробуем сделать это через цикл for :
Мы обрезали вывод, потому что алфавит сейчас не так интересен, не правда ли? Этот итератор, как и следовало ожидать, совершенно бесполезен. Мы могли бы просто перебирать ascii_lowercase напрямую. Но, надеемся, на этом примере вы лучше разобрались в итераторах.
Nextables
Для этого удалим метод __iter__ из предыдущего примера, в результате чего получим следующее:
Python отделяет итератор от последовательности
Мы начали экспериментировать со встроенными последовательностями и сделали небольшое забавное открытие. В Python последовательности сами по себе не являются итераторами. Скорее у каждой есть соответствующий класс-итератор, отвечающий за итерацию. Давайте посмотрим на диапазон в качестве примера:
Просто для проверки используем next с range_iterator :
Создание отдельных Iterable и Nextable
Вооружившись этими новыми знаниями об отделении итерируемого объекта от итератора, мы придумали новую идею:
Итерация действительно проста:
Это просто оболочка для нашей следующей таблицы из примера nextable. Затем пишем цикл:
Однако такая установка хрупкая. Возвращаясь к нашему правильно реализованному итератору, этот код будет работать:
А код с нашей комбинацией iterable + nextable — нет:
Заключение
Давайте подведем итоги! Во-первых, теперь вы знаете, что все циклы for в Python используют итераторы! Кроме того, как мы увидели, итераторы в Python позволяют нам отделить код, выполняющий итерацию, от кода, работающего с каждым элементом. Мы также надеемся, что вы узнали немного больше о том, как работают итераторы в Python и что такое протокол итератора.
На этом пока все, и мы надеемся, вам понравился более глубокий взгляд на протокол итераторов в Python!
Понимание итераторов в Python
Особенности, с которыми вы часто можете столкнуться в повседневной деятельности
1. Использование генератора дважды
Как мы видим в этом примере, использование переменной squared_numbers дважды, дало ожидаемый результат в первом случае, и, для людей незнакомых с Python в достаточной мере, неожиданный результат во втором.
2. Проверка вхождения элемента в генератор
Возьмём всё те же переменные:
А теперь, дважды проверим, входит ли элемент в последовательность:
Получившийся результат также может ввести в заблуждение некоторых программистов и привести к ошибкам в коде.
3. Распаковка словаря
Для примера используем простой словарь с двумя элементами:
Результат будет также неочевиден, для людей, не понимающих устройство Python, «под капотом»:
Последовательности и итерируемые объекты
По-сути, вся разница, между последовательностями и итерируемымыи объектами, заключается в том, что в последовательностях элементы упорядочены.
Так, последовательностями являются: списки, кортежи и даже строки.
Отличия цикла for в Python от других языков
А с итерируемыми объектами, последовательностями не являющимися, не будет:
Цикл for использует итераторы
Как мы могли убедиться, цикл for не использует индексы. Вместо этого он использует так называемые итераторы.
Итераторы — это такие штуки, которые, очевидно, можно итерировать 🙂
Получить итератор мы можем из любого итерируемого объекта.
Для этого нужно передать итерируемый объект во встроенную функцию iter :
Реализация цикла for с помощью функции и цикла while
Чтобы сделать это, нам нужно:
Теперь мы знакомы с протоколом итератора.
А, говоря простым языком — с тем, как работает итерация в Python.
Функции iter и next этот протокол формализуют. Механизм везде один и тот же. Будь то пресловутый цикл for или генераторное выражение. Даже распаковка и «звёздочка» используют протокол итератора:
Генераторы — это тоже итераторы
Генераторы тоже реализуют протокол итератора:
В случае, если мы передаём в iter итератор, то получаем тот же самый итератор
Итератор не имеет индексов и может быть использован только один раз.
Протокол итератора
Теперь формализуем протокол итератора целиком:
Итераторы работают «лениво» (en. lazy). А это значит, что они не выполняют какой-либо работы, до тех пор, пока мы их об этом не попросим.
Таким образом, мы можем оптимизировать потребление ресурсов ОЗУ и CPU, а так же создавать бесконечные последовательности.
Итераторы повсюду
Мы уже видели много итераторов в Python.
Я уже упоминал о том, что генераторы — это тоже итераторы.
Многие встроенные функции является итераторами.
Так, например, enumerate :
Создание собственного итератора
Так же, в некоторых случаях, может пригодится знание того, как написать свой собственный итератор и ленивый итерируемый объект.
В моей карьере этот пункт был ключевым, так как вопрос был задан на собеседовании, которое, как вы могли догадаться, я успешно прошёл и получил свою первую работу:)
Таким образом мы написали бесконечный и ленивый итератор.
А это значит, что ресурсы он будет потреблять только при вызове.
Не говоря уже о том, что без собственного итератора имлементация бесконечной последовательности была бы невозможна.
А теперь вернёмся к тем особенностям, которые были изложены в начале статьи
1. Использование генератора дважды
В данном примере, список будет содержать элементы только в первом случае, потому что генераторное выражение — это итератор, а итераторы, как мы уже знаем — сущности одноразовые. И при повторном использовании не будут отдавать никаких элементов.
2. Проверка вхождения элемента в генератор
А теперь дважды проверим, входит ли элемент в последовательность:
В данном примере, элемент будет входить в последовательность только 1 раз, по причине того, что проверка на вхождение проверяется путем перебора всех элементов последовательности последовательно, и как только элемент обнаружен, поиск прекращается. Для наглядности приведу пример:
Как мы видим, при создании списка из генераторного выражения, в нём оказываются все элементы, после искомого. При повторном же создании, вполне ожидаемо, список оказывается пуст.
3. Распаковка словаря
Так как распаковка опирается на тот же протокол итератора, то и в переменных оказываются именно ключи:
Выводы
Последовательности — итерируемые объекты, но не все итерируемые объекты — последовательности.
Итераторы — самая простая форма итерируемых объектов в Python.
Любой итерируемый объект реализует протокол итератора. Понимание этого протокола — ключ к пониманию любых итераций в Python.
Генераторы и итераторы в Python
Генератор в Python – одна из самых полезных и специальных функций. Мы можем превратить функцию в итератор, используя генераторы Python.
Базовая структура генератора
По сути, генератор в Python – это функция. Вы можете рассматривать следующее, как базовую структуру генератора.
В приведенной выше структуре вы можете видеть, что все похоже на функцию, за исключением одного ключевого слова yield. Это ключевое слово играет жизненно важную роль. Только использование yield превращает обычную функцию в генератор.
Обычная функция возвращает какое-то значение, генератор возвращает какое-то значение и автоматически реализует next() и _iter_.
Генератор написан как обычные функции, но использует оператор yield всякий раз, когда они хотят вернуть какие-то данные. Каждый раз, когда функция next() вызывается для функции генератора, он возобновляет работу с того места, где он остановился (он запоминает все значения данных и какой оператор был выполнен последним).
Давайте теперь изучим каждую строку предыдущего кода:
Если вы запустите указанную выше программу, она выдаст следующее:
Обратите внимание, что приведенный выше результат не является значением. Фактически это указывает, где находится объект. Чтобы получить реальное значение, воспользуйтесь итератором. Затем next() будет вызываться для объекта, чтобы получить следующее полученное значение.
Если вы хотите распечатать сгенерированные значения без цикла, вы можете использовать для него функцию next(). Если вы добавите еще одну строку в приведенный выше код, как показано ниже.
Затем он выведет значение 10, которое было передано в качестве аргумента и получено.
Получить значение генератора с точным вызовом next()
Теперь взгляните на следующую программу, в которой мы вызываем функцию next() генератора.
В приведенном выше коде вы должны знать точное количество полученных значений. В противном случае вы получите некоторую ошибку, так как функция генератора fruits() больше не генерирует значения.
Приведенный выше код будет выводиться следующим образом:
Получение значения генератора с косвенным вызовом next()
Вы можете получить значения генератора, используя цикл for. Следующая программа показывает, как можно распечатать значения с помощью цикла for и генератора. Это даст тот же результат.
Порядок работы
Давайте теперь посмотрим, как на самом деле работает генератор. Обычная функция завершается после оператора return, а генератор – нет.
В первый раз мы вызываем функцию, она возвращает первое значение, полученное вместе с итератором. В следующий раз, когда мы вызываем генератор, он возобновляет работу с того места, где он был приостановлен ранее.
Все значения не возвращаются одновременно из генератора, в отличие от нормальной функции. Это специальность генератора. Он генерирует значения, вызывая функцию снова и снова, что требует меньше памяти, когда мы генерируем огромное количество значений.
Вывод программы
Посмотрим другой код:
Помните, что range() – это встроенный генератор, который генерирует число в пределах верхней границы.
Итератор – это объект, который используется для итерации по итерируемому элементу.
Большинство объектов в Python являются итеративными. Все последовательности, такие как Python String, Python List, Python Dictionary и т.д., являются повторяемыми. Что такое итератор? Предположим, группа из 5 мальчиков выстроилась в линию. Вы указываете на первого мальчика и спрашиваете его, как его зовут. Затем он ответил. После этого вы спрашиваете следующего мальчика и так далее. Изображение ниже иллюстрирует это.
В этом случае вы Итератор. Очевидно, группа мальчиков – повторяющийся элемент.
Протокол Iterator
Протокол Iterator в Python включает две функции. Один – iter(), другой – next(). В этом разделе мы узнаем, как пройти по итерируемому элементу, используя протокол Iterator.
В предыдущем разделе мы привели пример группы из 5 мальчиков и вас. Вы итератор, а группа мальчиков – повторяемый элемент. Зная имя одного мальчика, вы задаете тот же вопрос следующему мальчику.
После этого вы делаете это снова. Функция iter() используется для создания итератора повторяемого элемента. А функция next() используется для перехода к следующему элементу.
Пример
Если итератор превысит количество повторяемых элементов, метод next() вызовет исключение StopIteration. Смотрите код ниже для примера:
Создание
Однако вы можете создать свои собственные указанные итераторы в Python. Для этого вам необходимо реализовать класс.
Как мы уже говорили ранее, протокол состоит из двух методов. Итак, нам нужно реализовать этот метод.
Например, вы хотите создать список чисел Фибоначчи, чтобы каждый раз при вызове следующей функции он возвращал вам следующее число.
Чтобы вызвать исключение, мы ограничиваем значение n ниже 10. Если значение n достигнет 10, это вызовет исключение. Код будет таким:
Итак, на выходе будет:
Зачем нужен итератор?
После прохождения предыдущего раздела у вас может возникнуть вопрос, зачем нам нужен Iterator.
Что ж, мы уже видели, что итератор может проходить по итерируемому элементу. Предположим, что в нашем предыдущем примере, если мы составим список чисел Фибоначчи, а затем проходим его через Iterator, это потребует огромной памяти. Но если вы создадите простой класс, вы сможете выполнить свою задачу, не потребляя столько памяти.
Итераторы в Python с примерами
Итератор в Python относится к объекту, по которому мы можем выполнять итерацию. iterator состоит из счетных значений, и эти значения можно просматривать одно за другим.
Итератор просто реализует протокол iterator в Python. Протокол итератора – это класс, который имеет два специальных метода, а именно __iter __() и __next __(). С помощью этих двух методов итератор может вычислить следующее значение в итерации.
С итераторами легко работать с последовательностями элементов в Python. Нам не нужно выделять вычислительные ресурсы всем элементам в последовательности, мы выполняем итерацию по одному элементу за раз, что помогает нам сэкономить место в памяти.
В этой статье мы изучим, как работать с итераторами в Python.
Итерируемые объекты
Итерируемый объект – это объект, способный возвращать итератор. Итерируемый объект может представлять, как конечные, так и бесконечные источники данных. Итерация прямо или косвенно реализует два метода: __iter __() и __next __(). Метод __iter __() возвращает объект-итератор, а метод __next __() помогает нам перемещаться по элементам в итеративном объекте.
Примеры итерируемых объектов в Python включают списки, словари, кортежи и наборы.
Создание итератора
В Python мы создаем итератор, реализуя для объекта методы __iter __() и __next __(). Рассмотрим следующий пример:
Мы создали итератор с именем element, который печатает числа от 0 до N. Сначала мы создали экземпляр класса и дали ему имя classinstance. Затем мы вызвали встроенный метод iter() и передали имя экземпляра класса в качестве параметра. Это создает объект-итератор.
Давайте теперь обсудим, как использовать итератор для фактического перебора элементов.
Метод next()
Метод next() помогает нам перебирать элементы итератора. Продемонстрируем это на примере, приведенном выше:
В приведенном выше скрипте мы вызвали метод next() и передали ему имя элемента итератора в качестве параметра. Каждый раз, когда мы это делаем, итератор переходит к следующему элементу в последовательности. Вот еще один пример:
В приведенном выше скрипте мы создали список с именем list1, который содержит 4 целых числа. Создан итератор с именем element. Метод next() помог нам перебрать элементы списка.
Итерация с помощью цикла for
Цикл for помогает нам перебирать любой объект, способный возвращать итератор. Например:
В приведенном выше коде мы создали переменную с именем x, которая используется для перебора элемента итератора через цикл for.
Бесконечные итераторы
Бесконечный итератор – это итератор с бесконечным числом итераций. Мы должны быть особенно осторожны при работе с бесконечными итераторами. Рассмотрим следующий пример:
Приведенный выше код будет работать вечно. Чтобы остановить это, вам придется вмешаться вручную. Вот еще один пример, демонстрирующий, как создать бесконечный итератор в Python:
Код должен возвращать все четные числа, начиная с 0. Мы можем запустить код, как показано ниже:
И эта цепочка может продолжаться вечно. Это показывает, что с бесконечным итератором мы можем иметь бесконечное количество элементов без необходимости хранить их все в памяти.
В следующем разделе мы увидим, как мы можем реализовать механизм выхода из таких бесконечных итераторов.
Остановка итерации
В предыдущем разделе мы увидели, как создать бесконечный итератор в Python. Однако итераторы обычно не предназначены для бесконечных итераций в Python. Всегда удобно реализовать условие завершения.
Мы можем остановить выполнение итератора навсегда с помощью оператора StopIteration. Нам нужно только добавить условие завершения в метод __next __(), которое вызовет ошибку, как только будет достигнуто указанное количество итераций. Вот пример: