Итерируемый объект, итератор и генератор в 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. Методы __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: легкий путеводитель для начинающих
В этом примере класс Numbers представляет итерируемый объект. Метод __iter__() возвращает сам объект, а метод __next__() возвращает следующий элемент числа, пока не достигнут предел.
Детальный ответ
Как сделать класс итерируемым в Python
Приветствую! В этой статье мы рассмотрим, как можно сделать класс итерируемым в языке программирования Python. Приготовьтесь, впереди нас ждет увлекательное путешествие!
Что значит «класс итерируемым»?
Прежде всего, давайте разберемся с термином «класс итерируемым». В языке Python, итерируемый класс — это класс, объекты которого можно итерировать, то есть проходить по его элементам. Зачастую, это требуется, когда вы хотите использовать циклы for для обхода объектов вашего класса.
1. Использование метода __iter__
Есть несколько способов сделать класс итерируемым, но наиболее распространенным является использование метода __iter__ . Давайте рассмотрим его работу на примере.
class MyClass: def __init__(self): self.data = [1, 2, 3, 4, 5] def __iter__(self): return iter(self.data)
В этом примере мы создаем класс MyClass , который имеет список данных data . Метод __iter__ возвращает итератор, созданный на основе списка data . Теперь объекты класса MyClass становятся итерируемыми и могут быть использованы в циклах for .
my_obj = MyClass() for item in my_obj: print(item)
В этом примере мы создаем экземпляр класса MyClass и проходимся по всем его элементам с помощью цикла for . Результатом будет вывод чисел от 1 до 5.
2. Использование методов __iter__ и __next__
Другим способом сделать класс итерируемым является использование методов __iter__ и __next__ . Позвольте мне продемонстрировать этот подход.
class MyIterator: def __init__(self, data): self.data = data self.index = 0 def __iter__(self): return self def __next__(self): if self.index >= len(self.data): raise StopIteration result = self.data[self.index] self.index += 1 return result class MyClass: def __init__(self): self.data = [1, 2, 3, 4, 5] def __iter__(self): return MyIterator(self.data)
В этом примере мы создаем класс MyClass , который имеет список данных data . Метод __iter__ возвращает экземпляр класса MyIterator , который реализует методы __iter__ и __next__ . Метод __next__ возвращает следующий элемент последовательности в каждой итерации.
my_obj = MyClass() for item in my_obj: print(item)
Этот код имеет такой же результат как и предыдущий пример: вывод чисел от 1 до 5.
3. Использование генератора
И последний, но не менее важный способ сделать класс итерируемым — использовать генератор.
class MyClass: def __init__(self): self.data = [1, 2, 3, 4, 5] def __iter__(self): for item in self.data: yield item
В этом примере мы создаем класс MyClass , который имеет список данных data . Метод __iter__ использует генератор, который возвращает каждый элемент последовательности data с помощью ключевого слова yield .
my_obj = MyClass() for item in my_obj: print(item)
Результатом будет тот же вывод чисел от 1 до 5.
Заключение
В этой статье мы рассмотрели различные способы, как можно сделать класс итерируемым в языке программирования Python. Мы изучили использование метода __iter__ с возвращением итератора, использование методов __iter__ и __next__ , а также использование генератора. Вы можете выбрать любой из этих способов в зависимости от ваших потребностей и предпочтений.
Основываясь на своем опыте, я убежден, что понимание итерируемости классов в Python может быть очень полезным как для начинающих разработчиков, так и для более опытных. Используйте эти знания, чтобы создавать элегантный и легко читаемый код.
Как сделать класс итерируемым в Python: легкий путеводитель для начинающих
В этом примере класс Numbers представляет итерируемый объект. Метод __iter__() возвращает сам объект, а метод __next__() возвращает следующий элемент числа, пока не достигнут предел.
Детальный ответ
Как сделать класс итерируемым в Python
Приветствую! В этой статье мы рассмотрим, как можно сделать класс итерируемым в языке программирования Python. Приготовьтесь, впереди нас ждет увлекательное путешествие!
Что значит «класс итерируемым»?
Прежде всего, давайте разберемся с термином «класс итерируемым». В языке Python, итерируемый класс — это класс, объекты которого можно итерировать, то есть проходить по его элементам. Зачастую, это требуется, когда вы хотите использовать циклы for для обхода объектов вашего класса.
1. Использование метода __iter__
Есть несколько способов сделать класс итерируемым, но наиболее распространенным является использование метода __iter__ . Давайте рассмотрим его работу на примере.
class MyClass: def __init__(self): self.data = [1, 2, 3, 4, 5] def __iter__(self): return iter(self.data)
В этом примере мы создаем класс MyClass , который имеет список данных data . Метод __iter__ возвращает итератор, созданный на основе списка data . Теперь объекты класса MyClass становятся итерируемыми и могут быть использованы в циклах for .
my_obj = MyClass() for item in my_obj: print(item)
В этом примере мы создаем экземпляр класса MyClass и проходимся по всем его элементам с помощью цикла for . Результатом будет вывод чисел от 1 до 5.
2. Использование методов __iter__ и __next__
Другим способом сделать класс итерируемым является использование методов __iter__ и __next__ . Позвольте мне продемонстрировать этот подход.
class MyIterator: def __init__(self, data): self.data = data self.index = 0 def __iter__(self): return self def __next__(self): if self.index >= len(self.data): raise StopIteration result = self.data[self.index] self.index += 1 return result class MyClass: def __init__(self): self.data = [1, 2, 3, 4, 5] def __iter__(self): return MyIterator(self.data)
В этом примере мы создаем класс MyClass , который имеет список данных data . Метод __iter__ возвращает экземпляр класса MyIterator , который реализует методы __iter__ и __next__ . Метод __next__ возвращает следующий элемент последовательности в каждой итерации.
my_obj = MyClass() for item in my_obj: print(item)
Этот код имеет такой же результат как и предыдущий пример: вывод чисел от 1 до 5.
3. Использование генератора
И последний, но не менее важный способ сделать класс итерируемым — использовать генератор.
class MyClass: def __init__(self): self.data = [1, 2, 3, 4, 5] def __iter__(self): for item in self.data: yield item
В этом примере мы создаем класс MyClass , который имеет список данных data . Метод __iter__ использует генератор, который возвращает каждый элемент последовательности data с помощью ключевого слова yield .
my_obj = MyClass() for item in my_obj: print(item)
Результатом будет тот же вывод чисел от 1 до 5.
Заключение
В этой статье мы рассмотрели различные способы, как можно сделать класс итерируемым в языке программирования Python. Мы изучили использование метода __iter__ с возвращением итератора, использование методов __iter__ и __next__ , а также использование генератора. Вы можете выбрать любой из этих способов в зависимости от ваших потребностей и предпочтений.
Основываясь на своем опыте, я убежден, что понимание итерируемости классов в Python может быть очень полезным как для начинающих разработчиков, так и для более опытных. Используйте эти знания, чтобы создавать элегантный и легко читаемый код.