Итераторы и итерируемые объекты в Python. Методы __next__ и __iter__
В английской документации по Python фигурируют два похожих слова – iterable и iterator. Обозначают они разное, хотя и имеющее между собой связь.
На русский язык iterable обычно переводят как итерируемый объект, а iterator – как итератор, или объект-итератор. С объектами обоих разновидностей мы уже сталкивались в курсе «Python. Введение в программирование», однако не делали на этом акцента.
Iterable и iterator – это не какие-то конкретные классы-типы, наподобие int или list . Это обобщения. Существует ряд встроенных классов, чьи объекты обладают возможностями iterable. Ряд других классов порождают объекты, обладающие свойствами итераторов.
Кроме того, мы можем сами определять классы, от которых создаются экземпляры-итераторы или итерируемые объекты.
Примером итерируемого объекта является список. Примером итератора – файловый объект. Список включает в себя все свои элементы, а файловый объект по-очереди «вынимает» из себя элементы и «забывает» то, что уже вынул. Также не ведает, что в нем содержится еще, так как это «еще» может вычисляться при каждом обращении или поступать извне. Например, файловый объект не знает сколько еще текста в связанном с ним файле.
Из такого описания должно быть понятно, почему один и тот же список мы можем перебирать сколько угодно раз, а с файловым объектом это можно сделать только единожды.
Зачем нужны объекты, элементы которых можно получить только один раз? Представьте, что текстовый файл большой. Если сразу загрузить его содержимое в память, то последней может не хватить. Также бывает удобно генерировать значения на лету, по требованию, если они нужны в программе только один раз. В противовес тому, как если бы они были получены все сразу и сохранены в списке.
У всех итераторов, но не итерируемых объектов, есть метод __next__. Именно его код обеспечивает выдачу очередного элемента. Каков этот код, зависит от конкретного класса. У файлового объекта это по всей видимости код, читающий очередную строку из связанного файла.
>>> f = open('text.txt') >>> f.__next__() 'one two\n' >>> f.__next__() 'three \n' >>> f.__next__() 'four five\n' >>> f.__next__() Traceback (most recent call last): File "", line 1, in StopIteration
Когда итератор выдал все свои значения, то очередной вызов __next__() должен возбуждать исключение StopIteration . Почему именно такое исключение? Потому что на него «реагирует» цикл for . Для for это сигнал останова.
Судя по наличию подчеркиваний у __next__ , он относится к методам перегрузки операторов. Он перегружает встроенную функцию next() . То есть когда объект передается в эту функцию, то происходит вызов метода __next__() этого объекта-итератора.
>>> f = open('text.txt') >>> next(f) 'one two\n' >>> next(f) 'three \n' >>> next(f) 'four five\n' >>> next(f) Traceback (most recent call last): File "", line 1, in StopIteration
Если объект итератором не является, то есть у него нет метода __next__ , то вызов функции next() приведет к ошибке:
>>> a = [1, 2] >>> next(a) Traceback (most recent call last): File "", line 1, in TypeError: 'list' object is not an iterator
Внутренний механизм работы цикла for так устроен, что на каждой итерации он вызывает функцию next() и передает ей в качестве аргумента объект, указанный после in в заголовке. Как только next() возвращает StopIteration , цикл for ловит это исключение и завершает свою работу.
Напишем собственный класс с методом __next__ :
>>> class A: . def __init__(self, qty): . self.qty = qty . def __next__(self): . if self.qty > 0: . self.qty -= 1 . return '+' . else: . raise StopIteration . >>> a = A(3) >>> next(a) '+' >>> next(a) '+' >>> next(a) '+' >>> next(a) Traceback (most recent call last): File "", line 1, in File "", line 9, in __next__ StopIteration
Вызов next() работает, но если мы попробуем передать объект циклу for , получим ошибку:
>>> b = A(5) >>> for i in b: . print(i) . Traceback (most recent call last): File "", line 1, in TypeError: 'A' object is not iterable
Интерпретатор говорит, что объект типа A не является итерируемым объектом. Другими словами, цикл for ожидает, что после in будет стоять итерируемый объект, а не итератор. Как же так, если цикл for потом вызывает метод __next__() , который есть только у итераторов?
На самом деле цикл for ожидает, что у объекта есть не только метод __next__ , но и __iter__ . Задача метода __iter__ – «превращать» итерируемый объект в итератор. Если в цикл for передается уже итератор, то метод __iter__() этого объекта должен возвращать сам объект:
>>> class A: . def __init__(self, qty): . self.qty = qty . def __iter__(self): . return self . def __next__(self): . if self.qty > 0: . self.qty -= 1 . return '+' . else: . raise StopIteration . >>> a = A(4) >>> for i in a: . print(i) . + + + +
Если циклу for передается не итератор, а итерируемый объект, то его метод __iter__() должен возвращать не сам объект, а какой-то объект-итератор. То есть объект, созданный от другого класса.
Получается, в классах-итераторах метод __iter__ нужен лишь для совместимости. Ведь если for работает как с итераторами, так и итерируемыми объектами, но последние требуют преобразования к итератору, и for вызывает __iter__() без оценки того, что ему передали, то требуется, чтобы оба – iterator и iterable – поддерживали этот метод. С точки зрения наличия в классе метода __iter__ итераторы можно считать подвидом итерируемых объектов.
Очевидно, по аналогии с next() , цикл for вызывает не метод __iter__() , а встроенную в Python функцию iter() .
Если список передать функции iter() , получим совсем другой объект:
>>> s = [1, 2] >>> si = iter(s) >>> type(s) >>> type(si) >>> si >>> next(si) 1 >>> next(si) 2 >>> next(si) Traceback (most recent call last): File "", line 1, in StopIteration
Как видно, объект класса list_iterator исчерпывается как нормальный итератор. Список s при этом никак не меняется. Отсюда понятно, почему после обхода циклом for итерируемые объекты остаются в прежнем составе. От них создается «копия»-итератор, а с ними самими цикл for не работает.
Напишем свой iterable-класс и связанный с ним iterator-класс, чтобы проиллюстрировать, как в Python может быть реализована взаимосвязь между итерируемым объектом и его итератором.
class Letters: def __init__(self, string): self.letters = [] for i in string: self.letters.append(f'--') def __iter__(self): return LettersIterator(self.letters[:]) class LettersIterator: def __init__(self, letters): self.letters = letters def __iter__(self): return self def __next__(self): if self.letters == []: raise StopIteration item = self.letters[0] del self.letters[0] return item kit = Letters('aeoui') print(kit.letters) for i in kit: print(i) print(kit.letters)
['-a-', '-e-', '-o-', '-u-', '-i-'] -a- -e- -o- -u- -i- ['-a-', '-e-', '-o-', '-u-', '-i-']
Заметим, что если в программе определяется класс, от которого будут создаваться итерируемые объекты, то в этой же программе должен быть класс, от которого создаются итераторы для этих объектов. При этом сам по себе класс, порождающий итераторы, никакой ценности может не иметь в том смысле, что непосредственно от него создание экземпляров в основной ветке программы не предполагается.
Обратное не верно. Если нужен класс, от которого создаются итераторы, определять класс для неких связанных с ним итерируемых объектов не требуется.
Практическая работа
Напишите класс-итератор, объекты которого генерируют случайные числа в количестве и в диапазоне, которые передаются в конструктор.
Курс с примерами решений практических работ:
pdf-версия
X Скрыть Наверх
Объектно-ориентированное программирование на Python
Итерируемый объект, итератор и генератор в Python
В Python итерируемый объект (iterable или iterable object), итератор (iterator или iterator object) и генератор (generator или generator object) — разные понятия, а не синонимы одного и того же. От итерируемого объекта можно получить его «копию»-итератор; генератор является разновидностью итератора.
В некоторых источниках итератор рассматривается как частный случай итерируемого объекта, поскольку оба поддерживают операцию итерации, то есть обход циклом for . Однако for работает только с итераторами. Переданный на обработку объект должен иметь метод __iter__() , который for неявно вызывает перед обходом. Метод __iter__() должен возвращать итератор.
У итерируемого объекта, то есть объекта, который можно «превратить» в итератор, должен быть метод __iter__() , который возвращает соответствующий объект-итератор.
>>> a = [1, 2] >>> b = a.__iter__() >>> a [1, 2] >>> b >>> type(a) >>> type(b)
У итерируемого объекта нет метода __next__() , который используется при итерации:
>>> a.__next__() Traceback (most recent call last): File "", line 1, in AttributeError: 'list' object has no attribute '__next__'
У итератора есть метод __next__() , который извлекает из итератора очередной элемент. При этом этот элемент уже не содержится в итераторе. Таким образом, итератор в конечном итоге опустошается:
>>> b.__next__() 1 >>> b.__next__() 2 >>> b.__next__() Traceback (most recent call last): File "", line 1, in StopIteration
Метод __next__() исчерпанного итератора возбуждает исключение StopIteration .
У итераторов, также как у итерируемых объектов, есть метод __iter__() . Однако в данном случае он возвращает сам объект-итератор:
>>> a = [1, 2] >>> a = "hi" >>> b = a.__iter__() >>> c = b.__iter__() >>> a 'hi' >>> b >>> c >>> b.__next__() 'h' >>> c.__next__() 'i' >>> b.__next__() Traceback (most recent call last): File "", line 1, in StopIteration
Здесь переменные b и c указывают на один и тот же объект.
Примеры итерируемых объектов в Python — список, словарь, строка и другие контейнерные типы (они же коллекции), тип, возвращаемый функцией range() .
Примеры итераторов — файловые объекты, генераторы, итераторы созданные на основе списка, строки, объекта типа range и т. д.
В Python есть встроенные функции iter() и next() , которые соответственно вызывают методы __iter__() и __next__() объектов, переданных в качестве аргумента.
>>> a = >>> b = iter(a) >>> b >>> next(b) 1
Внутренний механизм цикла for сначала вызывает метод __iter__() объекта. Так что, если передан итерируемый объект, создается итератор. После этого применяется метод __next__() до тех пор, пока не будет возбуждено исключение StopIteration .
Поскольку метод __iter__() итератора возвращает сам итератор, то после перебора циклом for объект исчерпывается. То есть получить данные из итератора можно только один раз. В случае с коллекциями это не так. Здесь создается другой объект — итератор. Он, а не итерируемый объект, отдается на обработку циклу for .
>>> a = range(2) >>> b = iter(a) >>> type(a) >>> type(b) >>> for i in a: . print(i) . 0 1 >>> for i in a: . print(i) . 0 1 >>> for i in b: . print(i) . 0 1 >>> for i in b: . print(i) . >>>
Отличительной особенностью генераторов является то, что они создаются не на основе классов, а путем вызова функции, содержащей инструкцию yield, или специальным генераторным выражением по синтаксису похожим на генератор списка. Отметим, генератор списка, который является особым выражением, к генераторам, которые являются разновидностью объектов-итераторов, отношения не имеет. Подробнее можно почитать здесь.
Другими словами, если потребуется создать свой итератор, может оказаться проще определить функцию с yield или воспользоваться выражением, чем создавать класс с методами __next__() и __iter__() .
Рассмотрим пример. Определим сначала собственный класс-итератор:
from random import random class RandomIncrease: def __init__(self, quantity): self.qty = quantity self.cur = 0 def __iter__(self): return self def __next__(self): if self.qty > 0: self.cur += random() self.qty -= 1 return round(self.cur, 2) else: raise StopIteration iterator = RandomIncrease(5) for i in iterator: print(i)
0.65 1.17 1.19 1.45 2.11
Наш итератор выдает числа по нарастающей. При этом каждое следующее число больше предыдущего на случайную величину.
Здесь же отметим преимущество итераторов как таковых перед контейнерными типами вроде списков. В памяти компьютера не хранятся все элементы итератора, в основном лишь описание, как получить следующий элемент. Если представить, что нужны тысячи чисел или надо генерировать сложные объекты, выгода существенна.
В случае с функцией, создающей генератор, приведенный выше пример может выглядеть так:
def random_increase(quantity): cur = 0 while quantity > 0: cur += random() quantity -= 1 yield round(cur, 2) generator = random_increase(5) for i in generator: print(i)
Нам незачем самим определять методы __iter__() и __next__() , так как они неявно присутствуют у генератора.
Если логика генератора проста, вместо функции можно использовать выражение, создающее генератор:
g = (round(random()+i, 2) for i in range(5)) for i in g: print(i)
Данный пример не идентичен приведенным выше функции и классу. Здесь целая часть каждого следующего числа больше чем у предыдущего на единицу.
Генераторное выражение и функция-генератор возвращают объект одного и того же типа — generator .
Итераторы — Python: Списки
На предыдущем уроке мы рассмотрели цикл for и термин «итерирование». И если в других языках это слово могут применять к любым циклам, то в Python у этого слова есть и другое значение. Еще итерирование — это взаимодействие с неким объектом, поддерживающим протокол итерации.
Для начала разберем, что же такое протокол в контексте Python. Протоколом называют набор определенных действий над объектом.
Если некий объект А позволяет совершать над собой действия, описанные неким протоколом Б, то говорят: «объект А реализует протокол Б» или «объект А поддерживает протокол Б».
В последующих курсах вы узнаете, что различных протоколов в Python — множество.
Даже многие синтаксические конструкции языка работают для самых разных объектов сходным образом именно потому, что объекты реализуют специальные протоколы.
Так мы можем в шаблон подставлять не только строки, но и значения других типов, потому что эти типы реализуют протокол приведения к строке. В Python протоколы встречаются на каждом шагу.
Протокол итерации
Протокол итерации — один из самых важных протоколов в Python. Ведь именно он позволяет циклу for работать с самыми разными коллекциями единообразно.
В чем же заключается этот протокол? Протокол требует от объекта быть итерируемым — то есть иметь специальный метод __iter__ .
Если у итерируемого объекта вызвать метод __iter__ , то метод должен вернуть новый специальный объект — так называемый итератор. В свою очередь, итератор должен иметь метод __next__ .
Звучит сложно, но давайте рассмотрим живой пример — итерирование списка. Список — итерируемый, поэтому нам подходит. Итак, создадим список и итератор для него:
l = [1, 2, 3, 5, 8, 11] i = iter(l) print(i) # =>
Мы вызвали для списка функцию 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 "", line 1, in # 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) # []
Пример демонстрирует два момента:
- zip — не перезапускаемый
- zip — перестает генерировать кортежи, как только заканчиваются элементы в любом из источников
Генераторы и ленивые вычисления
Большая часть языков программирования выполняет код в том порядке, в котором элементы кода написаны:
- Инструкции выполняются сверху вниз
- Выражения вычисляются после того, как будут вычислены их составляющие
- Функции вызываются после того, как будут вычислены их аргументы
Такая модель исполнения называется энергичной.
Существует и ленивая модель вычисления. В рамках этой модели вычисления производятся только тогда, когда их результат становится действительно нужен.
В любой программе при разных входных данных могут быть не нужны отдельные вычисления. Поэтому ленивая модель вычисления может дать определенные преимущества: то, что не нужно, не будет вычислено. Таким образом ленивость можно рассматривать как своего рода оптимизацию.
Python — это язык с энергичной моделью вычисления, поэтому практически всегда и все вычисляет сразу. Однако отдельные элементы ленивости присутствуют и в Python.
Генераторы — один из таких элементов. Генераторы производят элементы только по мере необходимости. И даже целые конструкции, собранные из генераторов — эдакие конвейеры, которые собирают составные значения и производят сборку по одному изделию за раз.
Так составной генератор zip(range(100000000), «abc») не генерирует все сто миллионов чисел, ведь строка «abc» слишком коротка, чтобы образовать столько пар. Но даже и этих пар не будет, если результат вычисления этого выражения не будет проитерирован.
Так ленивость позволяет экономить память при обработке больших потоков данных — нам не нужно загружать все данные целиком, достаточно загружать и обрабатывать их небольшими порциями.
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях:
Iterable (итерируемый)
Примерами типов, поддерживающих итерирование по своим элементам, являются:
- последовательности (например: список, строка, кортеж),
- а также другие типы (словарь, файл),
- включая пользовательские, для которых определены методы __iter__() или __getitem__().
- как в цикле for in,
- так и во многих других случаях, где ожидается последовательность (zip(), map()).
На заметку
Итератор используется для одиночного прохода по набору значений.
Для поддержки итерирования пользовательский тип должен реализовать метод __iter__(), возвращающий объект итератора.
При обращение с объектами, поддерживающими итерирование, обычно не обязательно явно вызывать iter() и оперировать итератором вручную, потому как for in сделает всё что нужно автоматически, создав временную безымянную переменную, в которую будёт помещён итератор на время выполнения цикла.
Подходы к созданию объектов, поддерживающих итерирование:
- Создать итератор;
- Создать генератор, или генераторное выражение (заключается в круглые скобки);
- Старый подход: определить .__getitem__().
Синонимы поиска: Iterable (итерируемый), итерация, поддерживающий итерирование
Статьи раздела
Mapping (отображение) | Отображениe — контейнер, элементы которого являются соответствиями од… |
Sequence (последовательность) | Последовательность — контейнер, элементы которого представляют собой … |
sets (множества) | Множества — неупорядоченные наборы уникальных объектов, поддерживающи… |