Итерируемый объект, итератор и генератор
Привет, уважаемые читатели Хабрахабра. В этой статье попробуем разобраться что такое итерируемый объект, итератор и генератор. Рассмотрим как они реализованы и используются. Примеры написан на Python, но итераторы и генераторы, на мой взгляд, фундаментальные понятия, которые были актуальны 20 лет назад и еще более актуальны сейчас, при этом за это время фактически не изменились.
Итераторы
Для начала вспомним, что из себя представляет паттерн «Итератор(Iterator)».
Назначение:
- для доступа к содержимому агрегированных объектов без раскрытия их внутреннего представления;
- для поддержки нескольких активных обходов одного и того же агрегированного объекта (желательно, но не обязательно);
- для предоставления единообразного интерфейса с целью обхода различных агрегированных структур.
Существуют два вида итераторов, внешний и внутренний.
Внешний итератор — это классический (pull-based) итератор, когда процессом обхода явно управляет клиент путем вызова метода Next.
Внутренний итератор — это push-based-итератор, которому передается callback функция, и он сам уведомляет клиента о получении следующего элемента.
Классическая диаграмма паттерна “Итератор”, как она описана в небезызвестной книги «банды четырех»:
Aggregate — составной объект, по которому может перемещаться итератор;
Iterator — определяет интерфейс итератора;
ConcreteAggregate — конкретная реализация агрегата;
ConcreteIterator — конкретная реализация итератора для определенного агрегата;
Client — использует объект Aggregate и итератор для его обхода.
Пробуем реализовать на Python классический итератор
Конкретная реализация итератора для списка:
Конкретная реализация агрегата:
Теперь мы можем создать объект коллекции и обойти все ее элементы с помощью итератора:
А так как мы реализовали метод first, который сбрасывает итератор в начальное состояние, то можно воспользоваться этим же итератором еще раз:
Реализации могут быть разные, но основная идея в том, что итератор может обходить различные структуры, вектора, деревья, хеш-таблицы и много другое, при этом имея снаружи одинаковый интерфейс.
Протокол итерирования в Python
В книге «банды четырех» о реализации итератора написано:
Минимальный интерфейс класса Iterator состоит из операций First, Next, IsDone и CurrentItem. Но если очень хочется, то этот интерфейс можно упростить, объединив операции Next, IsDone и CurrentItem в одну, которая будет переходить к следующему объекту и возвращать его. Если обход завершен, то эта операция вернет специальное значения(например, 0), обозначающее конец итерации.
Именно так и реализовано в Python, но вместо специального значения, о конце итерации говорит StopIteration. Проще просить прощения, чем разрешения.
Сначала важно определиться с терминами.
Рассмотрим итерируемый объект (Iterable). В стандартной библиотеке он объявлен как абстрактный класс collections.abc.Iterable:
У него есть абстрактный метод __iter__ который должен вернуть объект итератора. И метод __subclasshook__ который проверяет наличие у класса метод __iter__. Таким образом, получается, что итерируемый объект это любой объект который реализует метод __iter__
Но есть один момент, это функция iter(). Именно эту функцией использует например цикл for для получения итератора. Функция iter() в первую очередь для получения итератора из объекта, вызывает его метод __iter__. Если метод не реализован, то она проверяет наличие метода __getitem__ и если он реализован, то на его основе создается итератор. __getitem__ должен принимать индекс с нуля. Если не реализован ни один из этих методов, тогда будет вызвано исключение TypeError.
Итого, итерируемый объект — это любой объект, от которого встроенная функция iter() может получить итератор. Последовательности(abc.Sequence) всегда итерируемые, поскольку они реализуют метод __getitem__
Теперь посмотрим, что с итераторами в Python. Они представлены абстрактным классом collections.abc.Iterator:
__next__ Возвращает следующий доступный элемент и вызывает исключение StopIteration, когда элементов не осталось.
__iter__ Возвращает self. Это позволяет использовать итератор там, где ожидается итерируемых объект, например for.
__subclasshook__ Проверяет наличие у класса метода __iter__ и __next__
Итого, итератор в python — это любой объект, реализующий метод __next__ без аргументов, который должен вернуть следующий элемент или ошибку StopIteration. Также он реализует метод __iter__ и поэтому сам является итерируемым объектом.
Таким образом можно реализовать итерируемый объект на основе списка и его итератор:
Функция next() вызывает метод __next__. Ей можно передать второй аргумент который она будет возвращать по окончанию итерации вместо ошибки StopIteration.
Прежде чем переходить к генераторам, рассмотрим еще одну возможность встроенной функции iter(). Ее можно вызывать с двумя аргументами, что позволит создать из вызываемого объекта(функция или класс с реализованным методом __call__) итератор. Первый аргумент должен быть вызываемым объектом, а второй — неким ограничителем. Вызываемый объект вызывается на каждой итерации и итерирование завершается, когда возбуждается исключение StopIteration или возвращается значения ограничителя.
Например, из функции которая произвольно возвращает 1-6, можно сделать итератор, который будет возвращать значения пока не «выпадет» 6:
Небольшой класс ProgrammingLanguages, у которого есть кортеж c языками программирования, конструктор принимает начальное значения индекса по названию языка и функция __call__ которая перебирает кортеж.
Можем перебрать все языки начиная с C# и до последнего:
Еще один пример:
Генераторы
С точки зрения реализации, генератор в Python — это языковая конструкция, которую можно реализовать двумя способами: как функция с ключевым словом yield или как генераторное выражение. В результате вызова функции или вычисления выражения, получаем объект-генератор типа types.GeneratorType.
В объекте-генераторе определены методы __next__ и __iter__, то есть реализован протокол итератора, с этой точки зрения, в Python любой генератор является итератором.
Концептуально, итератор — это механизм поэлементного обхода данных, а генератор позволяет отложено создавать результат при итерации. Генератор может создавать результат на основе какого то алгоритма или брать элементы из источника данных(коллекция, файлы, сетевое подключения и пр) и изменять их.
Ярким пример являются функции range и enumerate:
range генерирует ограниченную арифметическую прогрессию целых чисел, не используя никакой источник данных.
enumerate генерирует двухэлементные кортежи с индексом и одним элементом из итерируемого объекта.
Yield
Для начало напишем простой генератор не используя объект-генератор. Это генератор чисел Фибоначчи:
Но используя ключевое слово yield можно сильно упростить реализацию:
Любая функция в Python, в теле которой встречается ключевое слово yield, называется генераторной функцией — при вызове она возвращает объект-генератор.
Объект-генератор реализует интерфейс итератора, соответственно с этим объектом можно работать, как с любым другим итерируемым объектом.
Рассмотрим работу yield:
- при вызове функции gen_fun создается объект-генератор
- for вызывает iter() с этим объектом и получает итератор этого генератора
- в цикле вызывает функция next() с этим итератором пока не будет получено исключение StopIteration
- при каждом вызове next выполнение в функции начинается с того места где было завершено в последний раз и продолжается до следующего yield
Создается стейт-машина в которой при каждом вызове __next__ меняется состояния и в зависимости от него вызывается тот или иной кусок кода. Если в функции yield в цикле, то соответственно состояние стейт-машины зацикливается пока не будет выполнено условие.
Свой вариант range:
Генераторное выражение (generator expression)
Если кратко, то синтаксически более короткий способ создать генератор, не определяя и не вызывая функцию. А так как это выражение, то у него есть и ряд ограничений. В основном удобно использовать для генерации коллекций, их несложных преобразований и применений на них условий.
В языках программирования есть такие понятия, как ленивые/отложенные вычисления(lazy evaluation) и жадные вычисления(eager/greedy evaluation). Генераторы можно считать отложенным вычислением, в этом смысле списковое включение(list comprehension) очень похожи на генераторное выражение, но являются разными подходами.
Первый вариант работает схожим с нашей функцией cool_range образом и может генерировать без проблем любой диапазон. А вот второй вариант создаст сразу целый список, со всеми вытекающими от сюда проблемами.
Yield from
Для обхода ограниченно вложенных структур, традиционный подход использовать вложенные циклы. Тот же подход можно использовать когда генераторная функция должна отдавать значения, порождаемые другим генератором.
Функция похожая на itertools.chain:
Но вложенные циклы можно убрать, добавив конструкцию yield from:
Основная польза yield from в создании прямого канала между внутренним генератором и клиентом внешнего генератора. Но это уже больше тема про сопрограммы(coroutines), которые заслуживают отдельной статьи. Там же можно обсудить методы генератора: close(), throw() и send().
И в заключении еще один пример. Функция принимающая итерируемый объект, с любым уровнем вложенности другими итерируемыми объектами, и формирующая плоскую последовательность:
Building iterable object using python
![]()
Building an iterable object from scratch is easy in python, in this tutorial you will be able to learn how iterator works. We will be building a custom floating range functioning class which will return a sequence of floating numbers, similar to the range() function in python which returns an integer sequence.
The range() function in python returns a sequence of numbers, starting from 0 by default, and increments by 1 (by default), and stops before a specified number.
In order to build a custom iterable object, your class should contain two functions __iter__() and __next__() , together they are called iterator protocol.
Most built-in types in python like list , tuple and string etc are iterable objects, which means they can be looped until the last element.
Let’s code
We will start by defining a simple python class which will include iter and next method.
Now that we are ready with our basic skeleton, let's add functionality to it. Starting with __init__() which is indeed constructor. For any given sequence all we need is starting and ending value.
Since it's a range, if there is no starting value then the default value will be considered as 0.0, also we have a decimal point value specified through which we can break down our range.
Understand the decimal logic
If decimal point value is not specified, then by default we will split our range up to 1 decimal point, x.0 to y.1 where x is the starting value and y is ending value.
If decimal value is 2, the sequence breakdown will be from .01 to .99, likewise it will increase with increase in value.
Now that we have initialized our range, we need to tell our interpreter to initialize our object as an iterator, for that we need to modify our __iter__() function.
Now that we have an iterator object initialized, we need to specify a new value on every next iteration. For that, we need to modify our __next__() function.
So, now each time we call a next() function with our object or just iterate it through a loop, it will increment and return a new value until the final value of the sequence.
Python: Make class iterable
I have inherited a project with many large classes constituent of nothing but class objects (integers, strings, etc). I’d like to be able to check if an attribute is present without needed to define a list of attributes manually.
Is it possible to make a python class iterable itself using the standard syntax? That is, I’d like to be able to iterate over all of a class’s attributes using for attr in Foo: (or even if attr in Foo ) without needing to create an instance of the class first. I think I can do this by defining __iter__ , but so far I haven’t quite managed what I’m looking for.
Python make class iterable – Python : How to make a class Iterable & create Iterator Class for it ?
How to make a class Iterable & create Iterator Class for it ?
Python make class iterable: We are going to see how we can make a class iterable and also creating an iterator class for it.
What’s the need to make a Custom class Iterable ?
User defined classes are not iterable by default. To make the class objects iterable we have to make the class iterable and also create an iterator class for it.
Let’s try to iterate the class with a for loop
The above code will throw an error as the School class is not iterable yet.
How to make your Custom Class Iterable | The Iterator Protocol :
In order to make the class iterable, we need to override the iter( ) function inside the class so that the function returns the object of the iterator class which is associated with the iterable class.
The __iter__( ) has been overridden in the School class which now returns the object from the schoolIterator class. And when we call iter( ) function on the school class it will call __iter__( ) function on the object.
How to create an Iterator Class :
In order to create an iterator class, we have to override the __next__( ) function so that every time we call a function, it should return the next iterable class until there are no elements. If there are no next elements, then it should raise the StopIteration.
After that we need to make the class object return the next element from the School class Object’s data member
The Working
The iter( ) function calls the overridden __iter__( ) function on the school objects, which would return, the SchoolIterator object. Upon calling the next( ) function, it would call our overridden function __next__( ) internally. The _index variable is being used here to keep track of the iterated elements. So every time we call the function it iterates the objects and in the need it raises the StopIteration.