Просто о сложном — итераторы в Python
Итератор, итерации — всё это в голове вызывает ассоциации с массивами и с циклическими операциями. В данной статье мы разберемся с вопросом, что такое итератор, чем он отличается от итерируемого объекта, как работает цикл for “под капотом” и, наконец, как реализовать свой собственный итератор.
Важно понимать, что итератор и итерируемое — понятия хоть и созвучные, но абсолютно разные. Начнём с итерируемого. Итерируемый объект — это объект, предоставляющий возможность поочередного прохода по своим элементам. Под итерируемым объектом понимают: списки, строки, кортежи, словари, файлы. Если говорить строго, то объект является итерируемым, если из него можно получить итератор.
Итератор — это то, что можно получить из итерируемого объекта. Мы можем сказать, что итератор — это тип, позволяющий реализовать поток данных и предлагающий средства для продвижения по нему. В Python итератором является любой объект, реализующий так называемый протокол итераторов, который должен содержать в себе два метода: __iter()__ и __next()__. Метод __iter()__ должен возвращать итератор. Метод __next()__ — следующий элемент потока данных.
Давайте рассмотрим небольшой пример. Предположим, у нас есть список arr из пяти элементов.
Как мы знаем, список — это итерируемый объект. Из любого итерируемого объекта можно получить итератор. Давайте “извлечем” итератор из списка arr.
Также мы знаем, что для реализации протокола итератора, объект должен реализоваться метод __next__(), попробуем его вызвать.
Как мы видим, вывелся первый элемент списка. Не трудно догадаться, что будет, при повторных вызовах метода __next__().
Как мы видим, итератор в лице метода __next__() предоставил нам инструмент для прохода по потоку данных, в нашем случае — по всем элементам списка, а дойдя до конца — вызвал специальное исключение StopIteration, сигнализирующее о том, что итератор исчерпал все доступные ему значения.
Данная конструкция для вывода элементов списка, конечно, очень объемна. Обычно для этой задачи мы используем цикл for.
А как for перебирает элементы? На самом деле, “под капотом”, цикл for — это синтаксический сахар для цикла while. А цикл while как раз-таки работает ни с чем иным как с итератором итерируемого объекта. Давайте посмотрим на то, как отрабатывает цикл for без прикрас:
Как мы видим, white делает ровно то, что мы сделали руками выше. Извлекает итератор, и пока есть данные выводит каждый элемент. По исчерпанию данных поднимается исключение StopIteration, который в нашем случае и заканчивает white, выведя информацию о нем в консоль, потому что в отличие от for-а, while не обрабатывает данное исключение.
При желании, мы можем записать наш код немного красивее, не прибегая к непосредственному упоминанию методов __iter__() и __next__(), а заменив их на методы-обертки iter() и next().
Ничего не мешает нам самим написать класс реализующий итератор, объект которого может быть передан цикл for для работы с потоком данных, содержащемся в объекте. Предположим мы хотим написать функционал, который бы возвращал квадрат чисел от текущего введенного и до одного. Давайте сделаем это с помощью итераторов.
Попробуем создать объект нашего класса и пройтись по нему циклом for:
А теперь тоже самое, но используя while и явную работу с итератором.
Как мы видим, наш итератор прекрасно себя чувствует и работает как полагается. В нашем случае объект нашего класса и есть итератор сам по себе, поэтому __iter__() возвращает self.
Таким образом, мы разобрались, что такое итерируемые объекты, что такое итераторы, как на самом деле происходит проход по итерируемому объекту и как создать свой итератор. Итераторы очень полезны в контексте генераторов, но об этом мы поговорим в следующей статье.
Итераторы
В этой лекции, основанной на серии тетрадок Jordan Moldow, мы рассмотрим такие понятия как итераторы и итерируемые объекты.
Итераторы
Итератор — это поведенческий паттерн проектирования, который даёт возможность последовательно обходить элементы составных объектов, не раскрывая их внутреннего представления.
В Python Iterator 1 является экземпляром любого класса, который реализует магические методы __iter__() и __next__() ( next() в Python 2.x) 2 .
iter(iterator) или, что тоже самое iterator.__iter__() , должен всегда возвращать iterator , указывая тем самым, что объект является итератором по отношению к себе.
next(iterator) эквивалентен вызову iterator.__next__() , а value = next(iterator, default) эквивалентен записи вида:
__next__() это метод, который «вычисляет» и возвращает следующий элемент итератора. Когда итератор исчерпан, то есть, нет больше элементов, которые он может вернуть, порождается исключение StopIteration . Таким образом, __next__() изменяет внутреннее состояние итератора и по умолчанию (и соглашению) итераторы исчерпываются после одного полного прохода по ним.
Как перебирать собственные объекты в Python
Это руководство поможет вам понять, что такое итераторы и итераторы, а также их взаимосвязь друг с другом. Во-вторых, понимание того, как цикл Python for работает под капотом, в конечном итоге поможет в разработке пользовательского настраиваемого объекта, который можно повторять.
Итерируемые объекты
Итерируемый объект — это объект, способный возвращать свои члены один за другим. Проще говоря, итерация — это все, что вы можете перебрать с помощью цикла for в Python. Последовательности — очень распространенный тип итераций. Примеры встроенных типов последовательностей включают списки, строки и кортежи.
Итераторы
Итератор — это объект, представляющий поток данных. Вы можете создать объект-итератор, реализовав встроенную функцию iter в итерацию.
Итератор можно использовать для ручного перебора элементов в итерируемом объекте. Повторная передача итератора встроенной функции next возвращает последовательные элементы в потоке. Когда элемент потребляется из итератора, он исчезает, и в конечном итоге, когда больше нет данных для извлечения, возникает исключение StopIteration.
Понимание Python for Loop
Центральное место в разработке знаний об итераторах и итераторах занимает понимание того, как цикл for в Python работает под капотом. Чтобы лучше проиллюстрировать это, давайте определим функцию, которая может принимать любую итерацию, и перебрать ее без с помощью цикла for.
Наша функция должна быть в состоянии достичь следующего:
· Создайте итератор из итерируемого
· Неоднократно получать следующий элемент из итератора
· Выполнить любое намеченное действие
· Вызвать исключение StopIteration, когда больше нет элементов для получения.
Под капотом итерация преобразуется в итератор в цикле Python for.
Наша настраиваемая функция сначала преобразует любой итератор в итератор. Затем в цикле while мы получаем следующий элемент от итератора и выполняем любое действие с этим элементом. В этом случае я решил написать функцию для увеличения числа в итераторе в степени 2, но можно предпринять любое действие, например, мы даже можем просто распечатать числа в нашем контейнере или коллекции.
Таким образом работают все формы перебора итераций в Python.
Ключевые определения
Чтобы лучше отличать итерацию от итератора, может быть полезно дополнительно уточнить их определения и отметить их различия. Итераторы нельзя индексировать / разрезать (так как они могут быть бесконечно длинными). Кроме того, в отличие от итераций, у них нет длины. В приведенном ниже примере при попытке получить длину объекта итератора my_iter_list вызывает исключение TypeError.
›Итерация — это то, что вы можете перебрать.
›Итератор — это объект, представляющий поток данных. Он выполняет итерацию по итерации.
Красивое и краткое определение итераторов, взятое из StackOverflow во время исследования для этой статьи, выглядит следующим образом:
iterator — более общее понятие: любой объект, класс которого имеет next метод ( __next__ в Python 3) и __iter__ метод, который выполняет return self
Итераторы позволяют пользователям работать и создавать ленивые итерации. Ленивые итерируемые объекты не работают, пока мы не попросим их предоставить следующий элемент. Эта функция может помочь нам справиться с бесконечно длинными итерациями, которые не помещаются в память. Это называется ленивой оценкой и помогает сэкономить как память, так и время процессора.
Протокол итератора
Как обсуждалось выше, объекты итератора должны поддерживать следующие 2 метода, которые вместе составляют протокол итератора Python:
Метод dunder / magic iter:
- iterator .__ iter __ ()
Вернуть сам объект итератора. Это необходимо, чтобы разрешить использование контейнеров (также называемых коллекциями) и итераторов с операторами for и in .
Следующий метод dunder / magic:
- iterator .__ next __ ()
Вернуть следующий элемент из контейнера. Если элементов больше нет, вызовите исключение StopIteration.
Создание ваших собственных итерируемых типов
Мы можем создать собственный итератор. Для этого нам понадобится класс, в котором определены методы __init__, __next__ и __iter__.
CustomIterTeams
Во-первых, давайте определим настраиваемый класс CustomIterTeams. Этот класс не имеет встроенного итеративного поведения, но мы можем реализовать код в нашем классе, чтобы наш пользовательский объект вел себя как итеративный.
Есть два способа заставить настраиваемый пользовательский объект вести себя как итерируемый. Первый способ включает определение двух дандерских или магических методов, а именно __iter __ () и __next __ (). Метод dunder iter просто должен вернуть сам объект. Это потому, что, когда мы пишем цикл for, это будет объект, который мы собираемся перебирать. Этот метод iter возвращает итератор.
Внутри цикла for в Python используются итераторы.
Наш настраиваемый объект теперь является итератором и может работать с методом dunder next для возврата последовательных элементов в потоке. Эти два метода работают вместе, чтобы включить протокол итератора.
В конструкторе __init__ мы устанавливаем индекс объекта со значением -1. Когда вызывается следующий метод, т. Е. Как это происходит, например, во время первой итерации в цикле for, значение индекса увеличивается на 1. Затем мы проверяем, превышает ли значение индекса длину списка команд, Пользователь решил добавить, когда объект был впервые создан. Если индекс меньше длины команд, мы просто возвращаем команду с индексом в пределах диапазона из списка команд.
Как только индекс становится таким же или большим, чем длина списка команд, мы снова сбрасываем индекс обратно на -1 (как он был изначально установлен в конструкторе инициализации) и вызываем исключение StopIteration .
Теперь у пользователя есть возможность перебирать созданные команды. Объект CustomIterTeams, prem_teams, теперь является итератором, по которому мы можем выполнять итерацию.
Индекс намеренно возвращается к исходному значению после того, как индекс достигает длины списка, прежде чем возникает исключение StopIteration. Эта функция реализована для того, чтобы пользователь мог выполнять несколько итераций объекта, если он хочет, в одном сеансе, как показано в приглашении python, показанном ниже.
Теперь мы также можем изменить порядок команд, просто реализовав зарезервированный метод dunder.
Более простой способ определить настраиваемый итеративный тип
Нет необходимости определять метод dunder next, чтобы сделать определяемый пользователем объект итеративным. Скорее, мы просто получаем метод dunder iter для возврата генератора, который проходит через наши команды. Каждый генератор — это итератор. Генераторы имеют встроенный метод next, поэтому нет необходимости реализовывать метод next в вашем пользовательском классе python.
Суть github для этого фрагмента кода находится здесь и показана ниже:
Резюме:
Итерация может быть достигнута в ваших настраиваемых классах либо путем включения методов iter и next, либо просто путем возврата генератора в методе iter. Выбор остается за программистом, но, хотя реализация методов iter и next немного длиннее, можно добавить более точно определенное поведение.
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.