Понимание итераторов в 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 . 🙂
Чтобы сделать это, нам нужно:
- Получить итератор из итерируемого объекта.
- Вызвать функцию next .
- Выполнить 'тело цикла'.
- Закончить цикл, когда будет получено исключение 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 , с помощью которой происходит итерирование итерируемого объекта.
Итератор не имеет индексов и может быть использован только один раз.
Протокол итератора
Теперь формализуем протокол итератора целиком:
- Чтобы получить итератор мы должны передать функции iter итерируемый объект.
- Далее мы передаём итератор функции next .
- Когда элементы в итераторе закончились, порождается исключение StopIteration .
- Любой объект, передаваемый функции iter без исключения TypeError — итерируемый объект.
- Любой объект, передаваемый функции next без исключения TypeError — итератор.
- Любой объект, передаваемый функции iter и возвращающий сам себя — итератор.
- Итераторы работают "лениво" (en. lazy). А это значит, что они не выполняют какой-либо работы, до тех пор, пока мы их об этом не попросим.
- Таким образом, мы можем оптимизировать потребление ресурсов ОЗУ и 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.
Итераторы в Python для самых маленьких
«Напиши, пожалуйста, кастомный итератор,» — такое задание довольно часто дают на собеседованиях, и я раз за разом вижу обреченные глаза кандидата, когда он сталкивается с подобной просьбой. Опыт участия в собеседованиях показал мне, что большинство начинающих разработчиков бегут от этой темы, потому что она кажется слишком запутанной и непонятной. А ведь ничего сложного в ней нет, если подобраться к ней правильным образом — в чём я и постараюсь помочь дорогим читателям.
Наше путешествие мы начнем с того, что вообще такое итератор. Итератор — это некий объект, который в себе реализует интерфейс перебора чего-либо. А говоря рабоче-крестьянским языком — это такая штука, которая в себе описывает правило, по которому мы будем перебирать содержимое той или иной коробки.
Давайте представим, что у нас есть тумбочка. В этой тумбочке лежат несколько предметов: ножницы, карандаш, яблоко и книга. И перед нами, как перед разработчиками ПО, поставили задачу: описать с помощью кода тумбочку, которая могла бы принимать на хранение некие объекты и выдавать содержимое по требованию.
Конечно же, мы можем хранить наше содержимое тумбочки в любой удобной коллекции, например, в списке:
tumb = ["ножницы", "карандаш", "яблоко", "книга"]
И решать задачу добавления объектов посредством методов списка (через append , например), а задачу перебора — с помощью цикла for :
for obj in tumb: print(obj)
«Ну и при чём тут какой-то там итератор?» — спросите меня вы. А что, если я скажу вам, что цикл for работает не совсем так, как вы думаете?
Во многих статьях и книгах пишут про то, что цикл for позволяет перебирать объекты коллекций. Это правда. Однако часто из внимания упускают то, как именно он это делает. А ведь это фундаментально важно для понимания нашей темы.
На самом деле цикл for взаимодействует не с самим целевым объектом перебора, а с его итератором! В нашем случае — с итератором списка. То есть он как бы говорит: «Эй, объект! Я хочу тебя перебрать, поэтому дай мне то, что описывает правило твоего перебора!» Если объект сможет ответить на это «Вот, держи!» и вернет циклу for некий объект, то объект называется итерируемым и его можно перебирать. Если же он отвечает что-то вроде «Я не понимаю, о чём ты», то программа выдаст ошибку, и это будет означать, что мы попытались перебрать объект, который для этого не предназначен.
Весь фокус в том, что итерируемый объект в случае успеха в качестве ответа на запрос «Дай правило итерации!» возвращает объект итератор.
Как цикл for получает объект-итератор от целевого итерируемого объекта? С помощью неявного вызова встроенной функции iter , в которую в качестве аргумента он передаёт как раз этот самый целевой итерируемый объект. И если в результате будет получен некий объект, то дальнейшая работа будет производиться уже с ним. Давайте посмотрим, что мы получим в качестве результата, если передадим наш список в метод iter :
>>> print(iter(tumb))
Видите? Мы получили объект типа list_iterator , инкапсулирующий в себе то самое правило перебора, которое сейчас будет применяться.
После успешного получения итератора цикл for начинает взаимодействовать с ним тупым нажиманием кнопки «давай следующее значение» до тех пор, пока эти значения не будут исчерпаны. Представьте себе, что вам дали в руки пульт с нопкой и сказали нажимать на неё до тех пор, пока вы не получите на экране сообщение «Хватит!»
Как цикл for нажимет эту воображаемую кнопку у итератора? С помощью ещё одной встроенной функции next , аргументом которой является объект-итератор, полученный на предыдущем шаге. Результатом этого в штатном случае будет получение очередного значения. Такая процедура будет повторяться многократно до тех пор, пока цикл for не получит сообщение о том, что все значения уже закончились. Что это за сообщение? Это raise ошибки StopIteration .
А теперь давайте взглянем на аналог цикла for , написанный через while :
tumb = ["ножницы", "карандаш", "яблоко", "книга"] # получаем итератор для итерируемого объекта it = iter(tumb) try: while True: next_val = next(it) print("Очередное значение:", next_val) except StopIteration: # явно напечатаем сообщение об окончании итерации, # хотя цикл for этого не делает и ошибка просто подавляется print("Итерация закончена") print("Программа завершена")
То есть ответственность за перебор лежит не на цикле for (он просто запрашивает итератор и жмëт в нëм кнопку) и не на самом итерируемом объекте (он лишь должен отдавать свой объект-итератор по запросу), а на итераторе!
Хорошо, с этим понятно, но что там с каким-то кастомным итератором? Это про что вообще история? А эта история про ситуации, когда вам необходимо, чтобы объекты ваших самописных классов тоже можно было итерировать. Давайте представим, что нам пришлось написать для тумбочки отдельный класс, чтобы навесить там много разных дополнительных методов, суть которых нам не важна в описываемом контексте, но важно то, что мы хотели бы иметь возможность перебирать нашу тумбочку, как если бы это был простой список.
Пускай у нашей тумбочки предметы хранятся в составе списковых полей, каждое из которых будет соответствовать тому или иному ящику. У тумбочки будут методы добавления и удаления элементов в ящики. Наш класс будет выглядеть как-то так:
class Tumbochka: """Волшебная тумбочка с тремя ящиками для чего угодно""" def __init__(self): self.boxes = < 1: [], 2: [], 3: [] >def add_to_box(self, obj, box_num): if box_num not in : print("Вы ввели неправильный номер ящика!") else: self.boxes[box_num].append(obj) def remove_from_box(self, box_num): if box_num not in : print("Вы ввели неправильный номер ящика!") else: return self.boxes[box_num].pop() def __str__(self): boxes_items = self.boxes[1] + self.boxes[2] + self.boxes[3] return ", ".join(boxes_items)
Создадим тумбочку, нагрузим её предметами и выведем информацию на экран:
tumb = Tumbochka() tumb.add_to_box("ножницы", 1) tumb.add_to_box("карандаш", 2) tumb.add_to_box("яблоко", 3) tumb.add_to_box("книга", 1) print(tumb)
А теперь вопрос: как нам сделать так, чтобы нашу тумбочку можно было итерировать? Можно, конечно, взять и сделать что-то вроде нового списка, который будет хранить в себе сумму элементов трех ящиков: tumb.boxes[1] + tumb.boxes[2] + tumb.boxes[3] и итерировать его, но это не очень хороший подход. Почему? Давайте представим себе, что у нас есть список с несколькими итерируемыми объектами: списком, множеством и строкой.
my_shiny_list = [ ["Это", "список", "внутри", "списка"], , "Это строка внутри списка", ]
И у нас вознкает необходимость добавить к этим товарищам ещё и нашу тумбочку (а может даже и не одну):
my_shiny_list = [ ["Это", "список", "внутри", "списка"], , "Это строка внутри списка", tumb, ]
Согласитесь, что писать сумму списков внутри вложенного списка — не самое изящное решение, да и сам класс в процессе развития нашего проекта может измениться: например, уберутся или добавятся новые ящики. Придется помнить про все подобные места и вносить правки в них.
Теперь давайте представим, что по логике работы нашей программы у нас предполагается последовательное итерирование каждого из элементов нашего списка:
for some_collection in my_shiny_list: for el in some_collection: print(el)
В какой-то момент мы получим ошибку TypeError: 'Tumbochka' object is not iterable . Python говорит нам, что мы попытались проитерировать объект, который на приказ «Дай мне свой итератор!» отвечает что-то вроде «Я не понимаю, о чëм ты!».
Почему так произошло? Дело в том, что наш класс тумбочки понятия не имеет, кто отвечает за правило перебора элементов в ней.
Как это устранить? Нужно сделать так, чтобы встроенная функция iter получала от нашего объекта тумбочки её итератор. Для этого нам потребуется дописать в классе тумбочки магический метод __iter__ , назначение которого как раз и состоит в том, чтобы создавать и возвращать в результате своей работы некий объект-итератор.
Но давайте посмотрим повнимательнее — для решения нашей задачи достаточно, чтобы в качестве итератора тумбочки выступал итератор списка суммы трëх ящиков. Для его получения нам нужно будет просто передать эту сумму во встроенную функцию iter и уже результат по работы вернуть в качестве результата магического метода __iter__ ! Вот как это будет выглядеть:
def __iter__(self): # получаем сумму предметов всех ящиков boxes_items = self.boxes[1] + self.boxes[2] + self.boxes[3] # получаем итератор от списка и возвращаем его it = iter(boxes_items) return it
Теперь наша тумбочка без проблем сможет быть перебранной наравне с известными встроенными коллекциями и по праву будет являться итерируемым объектом.
И что, это и есть кастомный итератор? Нет! В вышеупомянутом примере мы воспользовались итератором списка в качестве итератора нашей тумбочки. То есть сам итератор не имеет ни малейшего понятия, что его вернули как результат работы какой-то там тумбочки; его задача состоит лишь в том, чтобы перебирать.
А теперь давайте с вами представим, что мы хотим, чтобы наша тумбочка при итерации возвращала не просто объекты, а ещё и их адреса в памяти. Тут возникает проблема: итератор списка нам уже не поможет. Нужно что-то помощнее.
Можно обратиться за помощью к генераторам (это такая разновидность итератора, про которую вы можете прочесть замечательную статью моего товарища, а также посмотреть серию его видео по теме). И реализация будет выглядеть примерно так:
def __iter__(self): # получаем сумму предметов всех ящиков boxes_items = self.boxes[1] + self.boxes[2] + self.boxes[3] # возвращаем очередное значение # (пару "объект в ящике тумбочки + адрес в памяти") с помощью yield for el in boxes_items: yield el, id(el)
Результатом работы магического метода __iter__ будет generator_object , из которого по запросу мы сможем получать очередные пары значений.
Ну, а теперь это кастомный итератор? Всë ещë нет! Необходимость написания кастомного итератора возникает тогда, когда мы хотим тонко управлять процессом перебора. Например, иметь возможность при каком-то событии начать итерацию с самого начала или установить значение указателя перебора на определённый элемент. То есть не перебирать всё подряд, как в цикле for , а управлять вручную нашим процессом через непосредственное взаимодействие с итератором.
Как решить эту задачу? Объект-генератор нам не подходит, использовать итераторы коллекций — тоже. Придëтся писать что-то своё 🙂 Это самое «что-то своё» и будет называться кастомным итератором. Это отдельный класс, объект которого будет возвращаться в качестве результата работы метода __iter__ . Давайте напишем простой класс:
class TumbochkaIterator: pass
А в классе тумбочки поменяем магический метод __iter__ на вот такую реализацию:
def __iter__(self): return TumbochkaIterator()
И попробуем получить итератор тумбочки через встроенную функцию iter :
Мы увидим ошибку TypeError: iter() returned non-iterator of type 'TumbochkaIterator' , которая любезно сообщает о том, что тот объект, который мы вернули, итератором на самом деле не является.
А что является, спросите вы? А является итератором то, что обладает специальным магическим методом, способным возвращать очередное значение. Именно возвращать очередное значение! Таким магическим методом является метод __next__ . Этот метод будет отрабатывать каждый раз, когда объект итератора будет передаваться во встроенную функцию next .
Давайте добавим пустой метод __next__ в наш класс-итератор:
class TumbochkaIterator: def __next__(self): pass
Теперь посмотрим на результат, который выведет этот код:
>>> print(iter(tumb))
Видите? Мы теперь получаем в качестве результата работы функции iter от нашей тумбочки самый настоящий объект итератор! При этом он не имеет никакого понятия из чего он вернулся — из тумбочки, ящика или грузовика. Ему об этом знать не нужно, его задача будет состоять лишь в том, чтобы возвращать очередное значение, когда его передадут в next !
Давайте теперь попробуем вручную получить несколько очередных значений из итератора:
it = iter(tumb) print(next(it)) print(next(it)) print(next(it)) print(next(it)) None None None None
Мы получили четыре объекта None , потому что именно их возвращает нам метод __next__ . Давайте сделаем так, чтобы __next__ возвращал единичку:
class TumbochkaIterator: def __next__(self): return 1
Теперь при попытке запустить код выше мы получим четыре единички.
1 1 1 1
А теперь давайте свяжем наши объекты в тумбочке с итератором: пусть итератор при инициализации принимает в себя ссылку на все объекты трëх ящиков. Также заведëм счетчик, который поможет нам всегда знать, на каком именно объекте перебора мы сейчас находимся. А метод __next__ изменим таким образом, чтобы на каждый его вызов мы возвращали следующий элемент и увеличивали при этом значение счетчика на единицу для того, чтобы при следующем вызове __next__ вернуть очередное значение. Общий код классов будет выглядеть так:
class TumbochkaIterator: def __init__(self, some_objects): self.some_objects = some_objects self.current = 0 def __next__(self): if self.current < len(self.some_objects): result = self.some_objects[self.current] self.current += 1 return result
class Tumbochka: """Волшебная тумбочка с тремя ящиками для чего угодно""" def __init__(self): self.boxes = < 1: [], 2: [], 3: [] >def add_to_box(self, obj, box_num): if box_num not in : print("Вы ввели неправильный номер ящика!") else: self.boxes[box_num].append(obj) def remove_from_box(self, box_num): if box_num not in : print("Вы ввели неправильный номер ящика!") else: return self.boxes[box_num].pop() def __str__(self): boxes_items = self.boxes[1] + self.boxes[2] + self.boxes[3] return ", ".join(boxes_items) def __iter__(self): return TumbochkaIterator(self.boxes[1] + self.boxes[2] + self.boxes[3])
А теперь давайте запустим вот этот код:
tumb = Tumbochka() tumb.add_to_box("ножницы", 1) tumb.add_to_box("карандаш", 2) tumb.add_to_box("яблоко", 3) tumb.add_to_box("книга", 1)
it = iter(tumb) print(next(it)) print(next(it)) print(next(it)) print(next(it)) print(next(it)) print(next(it))
Вот, что мы получим в результате:
ножницы книга карандаш яблоко None None
Что это за None ? Откуда это всë взялось? Дело в том, что встроенной функции next не важно, что вернулось в качестве результата работы __next__ . Пусть даже это будет None .
А теперь давайте добавим пару методов. Один будет возвращать значение current на старт, другой — на выбранную позицию, но в пределах списка наших элементов. Выглядеть эти методы будут вот так:
def to_start(self): self.current = 0 def to_current(self, val): if val >= len(self.some_objects) or val < 0: print("Неверное значение для курсора!") else: self.current = val
Теперь мы можем при ручной итерации через next обнулять нашу итерацию или перемещать курсор к какому-то другому значению. Например, мы можем сказать, что если тот элемент, который я достал из тумбочки — это ножницы, то следующий элемент я буду пропускать и перемещать курсор на шаг вперед. Можете попробовать написать такое условие в качестве домашнего задания к этой статье 😉
А сейчас я предлагаю запустить процесс итерации нашей тумбочки в цикле for и посмотреть на результат:
for el in tumb: print(el)
Давайте взглянем на результат (скорее всего, у вас будет просто None , потому что значения будут лететь очень быстро, я рекомендую добавить sleep(.1) перед вызовом print ):
ножницы книга карандаш яблоко None None None None None None
Программа ушла в бесконечный цикл и остановить её можно только с помощью ручного останова. Почему так произошло? Да потому что циклу for тоже не важно, что вернулось в качестве очередного значения из __next__ — он будет жать кнопку «Дай!» до тех пор, пока не возникнет исключение StopIteration . Возникновение этого исключения мы и должны теперь предусмотреть в методе __next__ . Давайте сделаем это:
def __next__(self): if self.current < len(self.some_objects): result = self.some_objects[self.current] self.current += 1 return result raise StopIteration
Перезапустим наш код и взглянем на результат:
ножницы книга карандаш яблоко
Теперь всё работает замечательно! Остался последний штрих: по соглашению объекты-итераторы также должны являться итерируемыми объектами. Принято в качестве итераторов для самих итераторов использовать их самих. Звучит зубодробительно, но я думаю, что взглянув на метод __iter__ в составе нашего итератора вопросы уйдут:
class TumbochkaIterator: def __init__(self, some_objects): self.some_objects = some_objects self.current = 0 def to_start(self): self.current = 0 def to_current(self, val): if val >= len(self.some_objects) or val < 0: print("Неверное значение для курсора!") else: self.current = val def __iter__(self): return self def __next__(self): if self.current < len(self.some_objects): result = self.some_objects[self.current] self.current += 1 return result raise StopIteration
То есть объект возвращает сам себя в качестве итератора, если мы хотим проитерировать сам итератор. Это сделано для того, чтобы у мы могли перебирать объекты кастомных итераторов в цикле for точно так же, как это можно делать с generator_objects или итераторами коллекций, вроде list_iterator .
- Итерируемый объект — это объект, который можно перебирать.
- За правило перебора отвечает итератор, а не сам объект.
- Итерируемый объект при попытке его перебрать должен уметь возвращать свой итератор, чтобы уже с ним продолжалась работа.
- Метод, который возвращает итератор, называется __iter__ .
- Объект-итератор должен иметь метод __next__ , который возвращает очередное значение.
- Цикл for будет вызывать функцию next от итератора до тех пор, пока не получит исключение StopIteration .
- Возникновение StopIteration — это ответственность итератора, а именно его метода __next__ .
- Если StopIteration не возникнет никогда, то мы получим бесконечный цикл.
- Написание кастомного итератора может понадобиться в том случае, если необходимо тонко управлять процессом итерации. Для стандартных случаев зачастую достаточно использовать итераторы стандартных коллекций или объекты-генераторы.
Что такое итераторы и как их использовать в Python
Узнайте о мощных итераторах в Python: как они работают с разными структурами данных и как создавать собственные итераторы!
Алексей Кодов
Автор статьи
10 июля 2023 в 17:48
Итераторы — это ключевой концепт в Python, который позволяет создавать и работать с последовательностями объектов. Они являются основой многих встроенных структур данных, таких как списки, кортежи и множества, а также используются для создания пользовательских структур данных.
Определение итератора
Итератор — это объект, который реализует два метода: __iter__() и __next__() . Метод __iter__() возвращает сам итератор, а метод __next__() возвращает следующий элемент в последовательности. Если в последовательности больше нет элементов, метод __next__() должен вызвать исключение StopIteration .
Пример простого итератора:
class MyIterator: def __init__(self, max_value): self.max_value = max_value self.current_value = 0 def __iter__(self): return self def __next__(self): if self.current_value < self.max_value: self.current_value += 1 return self.current_value else: raise StopIteration my_iter = MyIterator(5) for i in my_iter: print(i)
1 2 3 4 5
Python-разработчик: новая работа через 9 месяцев
Получится, даже если у вас нет опыта в IT
Использование итераторов в Python
В Python итераторы используются с помощью ключевого слова for . Оно позволяет перебирать элементы последовательности, вызывая метод __next__() для каждого элемента до вызова исключения StopIteration .
Пример использования итератора с встроенным типом данных:
my_list = [1, 2, 3, 4, 5] for item in my_list: print(item)
1 2 3 4 5
Итераторы в Python можно использовать не только для встроенных структур данных, но и для пользовательских. Это позволяет создавать более сложные и гибкие структуры данных, которые могут быть обработаны с помощью стандартного синтаксиса for .
Итог
Итераторы — это мощный инструмент в Python, который позволяет работать с последовательностями объектов. Они используются для перебора элементов встроенных структур данных, таких как списки и кортежи, а также для создания пользовательских структур данных. Используйте итераторы для упрощения работы с последовательностями объектов и для повышения гибкости вашего кода.
Iterator (итератор)
Используется при проходе по элементам объектов, поддерживающих итерирование.
Повторяющиеся вызовы метода итератора __next__() (до +py3.0 next()) последовательно возвращают элементы из потока.
В случае исчерпания данных возникает исключение StopIteration. +py2.3 При этом последующие вызовы метода __next__() исчерпавшего себя итератора также возбуждают StopIteration.
Итераторы позволяют унифицировать операции в Питоне. Встретить их можно практически повсеместно.
my_str = 'abc'
my_iterator = iter(my_str)
my_iterator # < iterator object at 0x00A1DB50 >
my_iterator.next() # 'a'
my_iterator.next() # 'b'
my_iterator.next() # 'c'
my_iterator.next() # StopIteration
Для поддержки итерирования тип должен реализовать метод __iter__(), возвращающий объект итератора. Возможно, что объект, поддерживающий итерирование, сам явится итератором.
Стоит помнить, как может повести себя код с несколькими итеративными прогонами. Например, контейнер (например, список) порождает новый итератор каждый раз, когда вы передаёте его в функцию iter(), либо используете его в цикле for in. При попытке проделать то же с итератором будет возвращаться всё тот же ранее исчерпавший себя итератор из предыдущего прогона. А это создаст впечатление пустого контейнера.
В сущности, признаком принадлежности к типу является следование протоколу итератора, характеризующемуся наличием реализаций двух методов:
class Backwards:
def __init__(self, data):
self.data = data
self.index = len(data)
def __iter__(self):
return self
def __next__(self):
if self.index == 0:
raise StopIteration
self.index = self.index - 1
return self.data[self.index]
backwards = Backwards('some')
iter(backwards) # < Backwards instance at 0x7fceac135c68 >
for char in backwards:
print(char) # e m o s
На заметку
В стандартной библиотеке существует модуль itertools , предоставляющий функции, возвращающие итераторы, для эффективной обработки данных.