Что входит в протокол итерации python
Перейти к содержимому

Что входит в протокол итерации python

  • автор:

Тип данных Iterator, протокол итератора в Python

Тип iterator является обобщением понятия последовательности. Объект считается итерируемым, если он физически является последовательностью, либо он является объектом, который воспроизводит по одному результату за раз в контексте инструментов выполнения итераций, таких как цикл for . in .

В некотором смысле в категорию итерируемых объектов входят как физические последовательности, это списки, кортежи, диапазоны и т. д. , так и виртуальные последовательности, которые вычисляются по требованию.

Недостаток типа Iterator состоит в том, что при первом его вызове вычисляются сразу все значения последовательности как физической, так и виртуальной, к тому же все они хранятся в памяти до их исчерпания. Этот недостаток решает тип generator (генератор).

Чтобы обеспечить поддержку итерации, для объектов контейнера необходимо обязательно определить один метод container.__iter__() . Этот метод возвращает объект итератора. Объект необходим для поддержки протокола итератора, описанного ниже. Если контейнер поддерживает различные типы итераций, можно предоставить дополнительные методы для конкретного запроса итераторов для этих типов итераций.

Примером объекта, поддерживающего несколько форм итерации, может служить древовидная структура, поддерживающая обход как по ширине, так и по глубине. Метод container.__iter__() соответствует слоту tp_iter структуры типа для объектов Python в Python/C API.

Ограничения итератора.

  • Нельзя получить длину итератора функцией len() ;
>>> it = iter([i*i for i in range(10)]) >>> len(it) # Traceback (most recent call last): # File "", line 1, in # TypeError: object of type 'list_iterator' has no len() 
>>> it = iter([i*i for i in range(10)]) >>> it[2] # Traceback (most recent call last): # File "", line 1, in # TypeError: 'list_iterator' object is not subscriptable 
>>> it = iter([i*i for i in range(10)]) >>> it[2:5] # Traceback (most recent call last): # File "", line 1, in # TypeError: 'list_iterator' object is not subscriptable >>> import itertools >>> list(itertools.islice(it, 2, 5)) # [49, 64, 81] 
>>> it = iter([i*i for i in range(10)]) >>> list(it) # [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] >>> list(it) # [] 

Реализация/протокол типа Iterator :

Объекты типа Iterator должны поддерживать следующие два метода, которые вместе образуют протокол итератора :

container.__iter__() :

Метод вернет объект итератора. Это необходимо для того, чтобы разрешить использование контейнеров и итераторов с операторами for и in . Метод container.__iter__() соответствует слоту tp_iter структуры типа для объектов Python в API-интерфейсе. PyTypeObject.tp_iter — необязательный указатель на функцию, которая возвращает итератор для объекта. Его присутствие обычно сигнализирует о том, что экземпляры этого типа являются итеративными, хотя последовательности могут быть итерируемыми без этой функции.

container.__next__() :

Метод должен возвращать следующий элемент из контейнера, а если элементы в последовательности закончились, то метод container.__next__() должен бросить исключение StopIteration .

Python определяет несколько объектов итератора для поддержки итерации по общим и конкретным типам последовательностей, словарям и другим более специализированным формам. Конкретные типы не важны за пределами их реализации протокола итератора.

Как только метод container.__next__() итератора вызывает StopIteration , он должен продолжать вызывать его и при последующих вызовах. Реализации, которые не подчиняются этому свойству, считаются нарушенными.

Для создания объекта типа Iterator можно воспользоваться встроенной функцией iter() . По итератору можно двигаться с помощью функции next() .

Пример создания итератора Iterator :

class SimpleIterator: def __iter__(self): return self def __init__(self, limit): self.limit = limit self.counter = 0 def __next__(self): if self.counter  self.limit: ret = self.counter self.counter += 1 return ret else: raise StopIteration iters = SimpleIterator(5) # По итератору можно двигаться с помощью функции next() print('Функция next:', next(iters)) print('Функция next:', next(iters)) for i in iters: print('Цикл for . in: ', i) # Вызовем исключение `StopIteration` # т.к. итерация закончилась в цикле for next(iters) # Функция next: 0 # Функция next: 1 # Цикл for . in: 2 # Цикл for . in: 3 # Цикл for . in: 4 # Traceback (most recent call last): # File "/home/script/Simple-Iterator.py", line 26, in # next(iters) # File "/home/script/Simple-Iterator.py", line 15, in __next__ # raise StopIteration # StopIteration 

Понимание того, как работают итераторы и генераторы в языках программирования, это один из первых шагов к освоению последовательной обработки гигантских потоков данных. Трейдинг и технический анализ, это вещи, на которых многие делают целое состояние.

  • КРАТКИЙ ОБЗОР МАТЕРИАЛА.
  • Утиная типизация ‘Duck Typing’
  • Что такое вызываемый объект callable?
  • Как проверить тип переменной/объекта
  • Логический тип данных bool
  • Целые числа int
  • Ограничение длины преобразования целочисленной строки
  • Вещественные числа float
  • Комплексные числа complex
  • Типы последовательностей
  • Список list
  • Кортеж tuple
  • Диапазон range
  • Текстовые строки str
  • Словарь dict
  • Множество set и frozenset
  • Итератор Iterator, протокол итератора
  • Генератор generator и выражение yield
  • Контекстный менеджер with
  • Байтовые строки bytes
  • Байтовый массив bytearray
  • Тип memoryview, буфер обмена
  • Файловый объект file object
  • Универсальный псевдоним GenericAlias
  • Объект объединения Union

Понимание итераторов в Python

Python — особенный язык в плане итераций и их реализации, в этой статье мы подробно разберём устройство итерируемых объектов и пресловутого цикла for .

Особенности, с которыми вы часто можете столкнуться в повседневной деятельности

1. Использование генератора дважды

>>> numbers = [1,2,3,4,5] >>> squared_numbers = (number**2 for number in numbers) >>> list(squared_numbers) [1, 4, 9, 16, 25] >>> list(squared_numbers) []

Как мы видим в этом примере, использование переменной squared_numbers дважды, дало ожидаемый результат в первом случае, и, для людей незнакомых с Python в достаточной мере, неожиданный результат во втором.

2. Проверка вхождения элемента в генератор

Возьмём всё те же переменные:

>>> numbers = [1,2,3,4,5] >>> squared_numbers = (number**2 for number in numbers)

А теперь, дважды проверим, входит ли элемент в последовательность:

>>> 4 in squared_numbers True >>> 4 in squared_numbers False

Получившийся результат также может ввести в заблуждение некоторых программистов и привести к ошибкам в коде.

3. Распаковка словаря

Для примера используем простой словарь с двумя элементами:

>>> fruits_amount =
>>> x, y = fruits_amount

Результат будет также неочевиден, для людей, не понимающих устройство Python, «под капотом»:

>>> x 'apples' >>> y 'bananas'

Последовательности и итерируемые объекты

По-сути, вся разница, между последовательностями и итерируемымыи объектами, заключается в том, что в последовательностях элементы упорядочены.

Так, последовательностями являются: списки, кортежи и даже строки.

>>> numbers = [1,2,3,4,5] >>> letters = ('a','b','c') >>> characters = 'habristhebestsiteever' >>> numbers[1] 2 >>> letters[2] 'c' >>> characters[11] 's' >>> characters[0:4] 'habr'

Итерируемые объекты же, напротив, не упорядочены, но, тем не менее, могут быть использованы там, где требуется итерация: цикл for , генераторные выражения, списковые включения — как примеры.

# Can't be indexed >>> unordered_numbers = >>> unordered_numbers[1] Traceback (most recent call last): File "", line 1, in TypeError: 'set' object is not subscriptable >>> users = >>> users[1] Traceback (most recent call last): File "", line 1, in KeyError: 1 # Can be used as sequence >>> [number**2 for number in unordered_numbers] [1, 4, 9] >>> >>> for user in users: . print(user) . males females

Отличия цикла for в Python от других языков

Стоит отдельно остановиться на том, что цикл for , в Python, устроен несколько иначе, чем в большинстве других языков. Он больше похож на for. each , или же for. of .

Если же, мы перепишем цикл for с помощью цикла while , используя индексы, то работать такой подход будет только с последовательностями:

>>> list_of_numbers = [1,2,3] >>> index = 0 >>> while index < len(list_of_numbers): . print(list_of_numbers[index]) . index += 1 . 1 2 3

А с итерируемыми объектами, последовательностями не являющимися, не будет:

>>> set_of_numbers = >>> index = 0 >>> while index < len(set_of_numbers): . print(set_of_numbers[index]) . index += 1 . Traceback (most recent call last): File "", line 2, in TypeError: 'set' object is not subscriptable

Если же вам нужен index , то следует использовать встроенную функцию enumerate :

>>> set_of_numbers = >>> for index, number in enumerate(set_of_numbers): . print(number, index) . 1 0 2 1 3 2

Цикл for использует итераторы

Как мы могли убедиться, цикл for не использует индексы. Вместо этого он использует так называемые итераторы.

Итераторы — это такие штуки, которые, очевидно, можно итерировать 🙂
Получить итератор мы можем из любого итерируемого объекта.

Для этого нужно передать итерируемый объект во встроенную функцию iter :

>>> set_of_numbers = >>> list_of_numbers = [1,2,3] >>> string_of_numbers = '123' >>> >>> iter(set_of_numbers) >>> iter(list_of_numbers) >>> iter(string_of_numbers)

После того, как мы получили итератор, мы можем передать его встроенной функции next .

>>> set_of_numbers = >>> >>> numbers_iterator = iter(set_of_numbers) >>> next(numbers_iterator) 1 >>> next(numbers_iterator) 2

При каждом новом вызове, функция отдаёт один элемент. Если же в итераторе элементов больше не осталось, то функция next породит исключение StopIteration .

>>> next(numbers_iterator) 3 >>> next(numbers_iterator) Traceback (most recent call last): File "", line 1, in StopIteration

По-сути, это единственное, что мы может сделать с итератором: передать его функции next .
Как только итератор становится пустым и порождается исключение StopIteration , он становится совершенно бесполезным.

Реализация цикла for с помощью функции и цикла while

Используя полученные знания, мы можем написать цикл for , не пользуясь самим циклом for . 🙂

Чтобы сделать это, нам нужно:

  1. Получить итератор из итерируемого объекта.
  2. Вызвать функцию next .
  3. Выполнить 'тело цикла'.
  4. Закончить цикл, когда будет получено исключение StopIteration .
def for_loop(iterable, loop_body_func): iterator = iter(iterable) next_element_exist = True while next_element_exist: try: element_from_iterator = next(iterator) except StopIteration: next_element_exist = False else: loop_body_func(element_from_iterator)

Стоит заметить, что здесь мы использовали конструкцию try-else . Многие о ней не знают. Она позволяет выполнять код, если исключения не возникло, и код был выполнен успешно.

Теперь мы знакомы с протоколом итератора.
А, говоря простым языком — с тем, как работает итерация в Python.
Функции iter и next этот протокол формализуют. Механизм везде один и тот же. Будь то пресловутый цикл for или генераторное выражение. Даже распаковка и "звёздочка" используют протокол итератора:

coordinates = [1,2,3] x, y, z = coordinates numbers = [1,2,3,4,5] a,b, *rest = numbers print(*numbers)

Генераторы — это тоже итераторы

Генераторы тоже реализуют протокол итератора:

>>> def custom_range(number): . index = 0 . while index < number: . yield index . index += 1 . >>> range_of_four = custom_range(4) >>> next(range_of_four) 0 >>> next(range_of_four) 1 >>> next(range_of_four) 2 >>> next(range_of_four) 3 >>> next(range_of_four) Traceback (most recent call last): File "", line 1, in StopIteration

В случае, если мы передаём в iter итератор, то получаем тот же самый итератор

>>> numbers = [1,2,3,4,5] >>> iter1 = iter(numbers) >>> iter2 = iter(iter1) >>> next(iter1) 1 >>> next(iter2) 2 >>> iter1 is iter2 True

Итерируемый объект — это что-то, что можно итерировать.
Итератор — это сущность порождаемая функцией iter , с помощью которой происходит итерирование итерируемого объекта.

Итератор не имеет индексов и может быть использован только один раз.

Протокол итератора

Теперь формализуем протокол итератора целиком:

  1. Чтобы получить итератор мы должны передать функции iter итерируемый объект.
  2. Далее мы передаём итератор функции next .
  3. Когда элементы в итераторе закончились, порождается исключение StopIteration .
  1. Любой объект, передаваемый функции iter без исключения TypeError — итерируемый объект.
  2. Любой объект, передаваемый функции next без исключения TypeError — итератор.
  3. Любой объект, передаваемый функции iter и возвращающий сам себя — итератор.
  1. Итераторы работают "лениво" (en. lazy). А это значит, что они не выполняют какой-либо работы, до тех пор, пока мы их об этом не попросим.
  2. Таким образом, мы можем оптимизировать потребление ресурсов ОЗУ и CPU, а так же создавать бесконечные последовательности.

Итераторы повсюду

Мы уже видели много итераторов в Python.
Я уже упоминал о том, что генераторы — это тоже итераторы.
Многие встроенные функции является итераторами.

Так, например, enumerate :

>>> numbers = [1,2,3] >>> enumerate_var = enumerate(numbers) >>> enumerate_var >>> next(enumerate_var) (0, 1)
>>> letters = ['a','b','c'] >>> z = zip(letters, numbers) >>> z >>> next(z) ('a', 1)
>>> f = open('foo.txt') >>> next(f) 'bar\n' >>> next(f) 'baz\n' >>> 

В Python очень много итераторов, и, как уже упоминалось выше, они откладывают выполнение работы до того момента, как мы запрашиваем следующий элемент с помощью next . Так называемое, "ленивое" выполнение.

Создание собственного итератора

Так же, в некоторых случаях, может пригодится знание того, как написать свой собственный итератор и ленивый итерируемый объект.

В моей карьере этот пункт был ключевым, так как вопрос был задан на собеседовании, которое, как вы могли догадаться, я успешно прошёл и получил свою первую работу:)

class InfiniteSquaring: """Класс обеспечивает бесконечное последовательное возведение в квадрат заданного числа.""" def __init__(self, initial_number): # Здесь хранится промежуточное значение self.number_to_square = initial_number def __next__(self): # Здесь мы обновляем значение и возвращаем результат self.number_to_square = self.number_to_square ** 2 return self.number_to_square def __iter__(self): """Этот метод позволяет при передаче объекта функции iter возвращать самого себя, тем самым в точности реализуя протокол итератора.""" return self
>>> squaring_of_six = InfiniteSquaring(6) >>> next(squaring_of_six) 36 >>> next(squaring_of_six) 1296 >>> next(squaring_of_six) 1679616 >>> next(squaring_of_six) 2821109907456 >>> next(squaring_of_six) 7958661109946400884391936 >>> # И так до бесконечности. 
>>>iter(squaring_of_six) is squaring_of_six True

Таким образом мы написали бесконечный и ленивый итератор.
А это значит, что ресурсы он будет потреблять только при вызове.
Не говоря уже о том, что без собственного итератора имлементация бесконечной последовательности была бы невозможна.

А теперь вернёмся к тем особенностям, которые были изложены в начале статьи

1. Использование генератора дважды

>>> numbers = [1,2,3,4,5] >>> squared_numbers = (number**2 for number in numbers) >>> list(squared_numbers) [1, 4, 9, 16, 25] >>> list(squared_numbers) []

В данном примере, список будет содержать элементы только в первом случае, потому что генераторное выражение — это итератор, а итераторы, как мы уже знаем — сущности одноразовые. И при повторном использовании не будут отдавать никаких элементов.

2. Проверка вхождения элемента в генератор

>>> numbers = [1,2,3,4,5] >>> squared_numbers = (number**2 for number in numbers)

А теперь дважды проверим, входит ли элемент в последовательность:

>>> 4 in squared_numbers True >>> 4 in squared_numbers False

В данном примере, элемент будет входить в последовательность только 1 раз, по причине того, что проверка на вхождение проверяется путем перебора всех элементов последовательности последовательно, и как только элемент обнаружен, поиск прекращается. Для наглядности приведу пример:

>>> 4 in squared_numbers True >>> list(squared_numbers) [9, 16, 25] >>> list(squared_numbers) []

Как мы видим, при создании списка из генераторного выражения, в нём оказываются все элементы, после искомого. При повторном же создании, вполне ожидаемо, список оказывается пуст.

3. Распаковка словаря

При использовании в цикле for , словарь будет отдавать ключи:

>>> fruits_amount = >>> for fruit_name in fruits_amount: . print(fruit_name) . apples bananas

Так как распаковка опирается на тот же протокол итератора, то и в переменных оказываются именно ключи:

>>> x, y = fruits_amount >>> x 'apples' >>> y 'bananas'

Выводы

Последовательности — итерируемые объекты, но не все итерируемые объекты — последовательности.

Итераторы — самая простая форма итерируемых объектов в Python.

Любой итерируемый объект реализует протокол итератора. Понимание этого протокола — ключ к пониманию любых итераций в Python.

[ Сборник задач ]
Тема 15. Итераторы

Понятие итераторов и итерируемых объектов, их различия, специфика протоколов, особенности применения. Способы создания ленивых итераторов и генераторов.

Вопросы и ответы
5 вопросов по теме "Итераторы" + ответы
Условия задач
5 задач по теме двух уровней сложности: Базовый и *Продвинутый
Решения задач
Приводим код решений указанных выше задач

One

Итераторы – специальные объекты для перебора коллекций. Основная их задача – упростить навигацию по отдельным значениям последовательностей или словарей.

  1. На каждом шаге итерации он обращается к следующему элементу последовательности;
  2. В момент достижения последнего значения возникает ошибка, говорящая о том, что больше элементов нет.
  1. Итератор и итерабельный объект, их протоколы;
  2. Функции iter() , next() ;
  3. Ленивые ( lazy ) итераторы;
  4. Генераторы (предназначение, yield ).

Читайте также

Курс. Программирование на Python.

'Программирование на Python' - бесплатный онлайн курс для начинающих из 6 уроков с лекциями, видео, слайдами и домашними заданиями.

Two

Вопросы по теме "Итераторы"
1. Перечислите основные отличия между итератором и итерабельным объектом.

Оба объекта позволяют перебирать коллекции, но их внутренняя логика разнится. Основные отличия приведены ниже.

  1. По мере перебора истощается
  2. При повторном обращении продолжает перебор с последнего места остановки
  3. Не индексируется
  4. Для повторного перебора с начала нужно пересоздавать
  5. Занимает мало места в памяти
  1. Никогда не истощается
  2. При повторном обращении в другом месте программы начинает перебирать элементы с начала
  3. Индексируется
  4. Не требует пересоздания для повторной итерации
  5. Занимает больше места в памяти

2. В чем заключается смысл протокола итератора?

Любой объект в Питоне становится итератором только тогда, когда в него внедрен соответствующий протокол. Он включает 2 метода:

iter() – возвращает сам себя, вызывается в самом начале. Позволяет пользоваться циклом for и выражением in»;
next() – перебирает коллекцию по одному элементу и вызывает ошибку StopIteration, когда их не остается.

Пример – Интерактивный режим
---
>>> num_lst = [7, 10, 3]
>>> num_lst_iter = iter(num_lst)
>>> next(num_lst_iter)
7
>>> next(num_lst_iter)
10
>>> next(num_lst_iter)
3
>>> next(num_lst_iter)
StopIteration

Обозначенную последовательность чисел мы превратили в итератор с помощью функции iter() . После того как функция next() перебрала все значения, возникла ошибка StopIteration .

3. Приведите несколько примеров встроенных функций-итераторов, используемых в Питоне.

Итераторы используются достаточно часто, так как практически не тратят память компьютера. Именно поэтому в Питон встроено большое количество соответствующих функций. Вот некоторые из них:

open() – для работы с файлами;
zip() – формирует итератор кортежей из представленных последовательностей;
map() – модифицирует каждый элемент итерируемого объекта.

Чтобы проверить, является ли объект итератором, достаточно сравнить его с с самим собой, обернутым в функцию iter() .

Пример – Интерактивный режим
---
>>> file = open('article.txt')
>>> iter(file) is file
True
>>> file.close()
>>> lst = [2, 3]
>>> iter(lst) is lst
False

4. Когда применяются ленивые вычисления и в чем их логика?

Ленивые вычисления предполагают, что не нужно ничего делать до тех пор, пока в этом нет необходимости. Так, если мы не обратились к свойству класса (которое определяется некими математическими операциями, например), то не нужно заранее его рассчитывать. На этом базируется, в том числе, логика генераторов. Многие итераторы также являются ленивыми. Это не просто удобно, но позволяет экономить память и время на вычисление.

Нагляднее всего можно понять преимущество ленивых вычислений (и итераторов) на следующем примере.

Пример – Интерактивный режим
---
# Создадим итерабельный объект
>>> nums = range(1, 12121212112)
>>> nums[10543210010]
10543210011 # Практически моментальный вывод
>>> list(nums)
MemoryError

Функция range() – ленивая. Она заранее не формирует последовательность из гигантского количества чисел. А когда мы обращаемся к некому ее элементу по индексу (это разрешено, так как она создает итерабельный объект), он выводится почти сразу.

Если вы рискнете преобразовать этот массив чисел в список, то случится одно из двух: либо выведется ошибка о нехватке памяти на компьютере, либо очень-очень долго будет формироваться список всех объектов.

5*. Дайте пример одного бесконечного итератора, который встроен в Python. Опишите его функционал.

«Ленивость» большинства итераторов и генераторов позволяет создавать «бесконечные» коллекции. Ведь они, по факту, не загружаются полностью в память, а выводятся по одному по мере надобности.

В Python встроено немалое количество функций, позволяющих реализовывать такие сущности. Ряд из них расположен в модуле itertools . Рассмотрим функцию count() . Она формирует бесконечную последовательность чисел с определенным шагом (шаг может быть как целым числом, так и десятичным). Это своего рода «считалка».

Пример – Интерактивный режим
---
>>> from itertools import count
>>> count_begin = count(5, 10)
>>> for num in range(6):
… print(next(count_begin))
5
15
25
35
45
55
>>> for num in range(2):
… print(next(count_begin))
65
75

Повторное обращение к «каунтеру» продолжит счёт с того числа, на котором он остановился до этого.

Итераторы и итерируемые объекты в 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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *