Передать в функцию итератор python

Итераторы в Python

Концепция итераторов никоим образом не специфична для Python. В самом общем виде это объект, который используется для перебора в цикле последовательности элементов. Однако разные языки программирования реализуют данную концепцию по-разному или не реализуют вовсе. В Python каждый цикл for использует итератор, в отличие от многих других языков. В данной статье мы поговорим про итераторы в Python. Кроме того, мы рассмотрим итерируемые объекты (англ. iterables) и т.н. nextables.

Итерируемые объекты

Итерируемый объект – это объект, который можно проитерировать, т.е. пройтись по элементам объекта в цикле. Например, итерируемым объектом может быть список или кортеж. В Python, чтобы объект был итерируемым, он должен реализовывать метод __iter__ . Этот метод-болванка принимает в качестве входных данных только экземпляр объекта — self — и должен возвращать объект-итератор. Вы можете использовать встроенную функцию iter , чтобы получить итератор итерируемого объекта.

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

Итераторы

Перейдем к собственно итераторам, рабочей лошадке итерации (особенно в Python). Итераторы – это уровень абстракции, который инкапсулирует знания о том, как брать элементы из некоторой последовательности. Мы намеренно объясняем это в общем виде, поскольку «последовательность» может быть чем угодно, от списков и файлов до потоков данных из базы данных или удаленного сервиса. В итераторах замечательно то, что код, использующий итератор, даже не должен знать, какой источник используется. Вместо этого он может сосредоточиться только на одном, а именно: «Что мне делать с каждым элементом?».

Читайте также:  W3 org html form

Итерация без итератора

Чтобы лучше понять преимущества итераторов, давайте кратко рассмотрим итерацию без итераторов. Примером итерации без итератора является классический цикл for в стиле C. Этот стиль существует не только в C, но и, например, в C++, go и JavaScript.

Пример того, как это выглядит в JavaScript:

let numbers = [1,2,3,4,5] for(let i=0; i < numbers.length; i++)< // Extract element const current_number = numbers[i] // Perform action on element const squared = current_number ** 2 console.log(squared) >

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

Все циклы for в Python используют итераторы

В Python нет циклов for в стиле C. А циклы for в Python-стиле напоминают циклы for each в других языках. Это тип цикла, в котором используются итераторы. То есть каждый цикл for , который вы пишете на Python, должен использовать итератор.

Сначала давайте посмотрим на Python-эквивалент предыдущего примера, наиболее близкий к нему синтаксически:

numbers = [1,2,3,4,5] for i in range(len(numbers)): number = numbers[i] squared = number**2 print(squared)

Да, очевидно, что нужно было просто перебирать numbers , но мы хотели приблизить синтаксис к циклу for в JavaScript. Здесь нам нужно выполнить извлечение самостоятельно, поэтому мы не используем итератор по числам. Вместо этого мы создаем диапазон, который проходится по индексам чисел (итератор). Это относительно близко к циклу for на JavaScript, но этот код все равно работает на более высоком уровне абстракции.

numbers = [1,2,3,4,5] i = 0 while i < len(numbers): number = numbers[i] squared = number**2 print(squared) i += 1

Здесь мы инициализируем i , определяем, какое условие должно быть выполнено для выхода из цикла и как увеличивать i . Нам нужно очень явно управлять значением i , что не требуется при использовании диапазона.

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

В документации Python итератор определяется как класс, реализующий __next__ и __iter__ . По этому определению итератор также является итерируемым объектом (iterable), поскольку он реализует __iter__ . Кроме того, можно сказать, что это nextable-объект, поскольку он реализует метод __next__ .

Отметим, nextable – это не часто используемый термин, потому что его можно запросто превратить в итератор. Как видите, метод __iter__ для итераторов легко реализовать. Фактически, в определении итератора явно указано, что должен делать метод:

class MyABCIterator: . def __iter__(self): return self

Вот и все, метод просто возвращает ссылку на сам итератор. Итак, если вы скопируете этот код __iter__ в nextable, вы получите итератор. Мы назвали класс MyABCIterator , поскольку мы встроим его в итератор, который выполняет итерацию по алфавиту.

Теперь давайте превратим это в итератор, сделав «некстабельным». Метод __next__ должен возвращать следующий объект в последовательности. Он также должен вызывать StopIteration при достижении конца последовательности (т.н. «исчерпание итератора»). То есть, в нашем случае — когда мы дошли до конца алфавита.

Воспользуемся удобной функцией ascii_lowercase , импортированной из модуля string стандартной библиотеки Python. Получаем строку, состоящую из строчных букв английского алфавита.

>>> from string import ascii_lowercase >>> ascii_lowercase 'abcdefghijklmnopqrstuvwxyz'

Хорошо, теперь давайте посмотрим на код нашего класса, а затем мы объясним, как он работает:

from string import ascii_lowercase class MyABCIterator: def __init__(self): self.index = 0 def __next__(self): if self.index >= len(ascii_lowercase): raise StopIteration() char = ascii_lowercase[self.index] self.index +=1 return char def __iter__(self): return self

Чтобы знать, какой символ возвращать при каждом вызове __next__ , нам понадобится индекс. Поэтому мы добавляем __init__ в наш класс, где инициализируем self.index нулем. Далее при каждом вызове __next__ мы сначала проверяем, достигли ли мы конца алфавита. Если индекс выходит за пределы строки, мы вызываем StopIteration , как указано в документации Python. Затем мы извлекаем текущий символ и увеличиваем self.index . В противном случае мы бы начали с b вместо a . Наконец, мы увеличиваем индекс и возвращаем ранее извлеченный символ.

Теперь давайте попробуем сделать это через цикл for :

>>> for char in MyABCIterator(): . print(char) a b c d e .

Мы обрезали вывод, потому что алфавит сейчас не так интересен, не правда ли? Этот итератор, как и следовало ожидать, совершенно бесполезен. Мы могли бы просто перебирать ascii_lowercase напрямую. Но, надеемся, на этом примере вы лучше разобрались в итераторах.

Последнее замечание по протоколу итераторов. Обратите внимание, как цикл for выполняет всю работу по использованию протокола. Он автоматически получает итератор, используя __iter__ , и многократно перебирает его, используя __next__ . Это соответствует всем принципам Python, когда магические методы не используются напрямую, а скорее являются способом подключиться к синтаксису Python или функциям верхнего уровня.

Nextables

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

Для этого удалим метод __iter__ из предыдущего примера, в результате чего получим следующее:

class MyABCNextable: def __init__(self): self.index = 0 def __next__(self): if self.index >= len(ascii_lowercase): raise StopIteration() char = ascii_lowercase[self.index] self.index +=1 return char

Попытка создать цикл for с таким объектом выдает нам TypeError: 'MyABCNextable' object is not iterable . Что, знаете ли, не удивительно. Интерпретатор не может найти __iter__ для вызова и, следовательно, не может создать итератор.

Python отделяет итератор от последовательности

Мы начали экспериментировать со встроенными последовательностями и сделали небольшое забавное открытие. В Python последовательности сами по себе не являются итераторами. Скорее у каждой есть соответствующий класс-итератор, отвечающий за итерацию. Давайте посмотрим на диапазон в качестве примера:

>>> range_0_to_9 = range(10) >>> type(range_0_to_9)

range() возвращает нам объект типа range . Теперь посмотрим, что произойдет, когда мы попытаемся использовать next для такого объекта:

>>> next(range_0_to_9) Traceback (most recent call last): File "", line 1, in TypeError: 'range' object is not an iterator

Мы получили ошибку TypeError: 'range' object is not an iterator . Итак, если объект типа range не является итератором, то что мы получим при использовании iter ?

>>> range_0_to_9_iterator = iter(range_0_to_9) >>> type(range_0_to_9_iterator)

Просто для проверки используем next с range_iterator :

>>> next(range_0_to_9_iterator) 0 >>> next(range_0_to_9_iterator) 1

Здесь мы возвращаем объект range_iterator , который фактически отвечает за выполнение итерации. Видимо, это относится ко всем встроенным типам, включая списки, словари и наборы. Похоже, что в CPython разработчики решили отделить итерацию объекта от самого объекта.

Создание отдельных Iterable и Nextable

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

Можем ли мы вернуть nextable из iterable ?

Итерация действительно проста:

class MyABCIterable: def __iter__(self): return MyABCNextable()

Это просто оболочка для нашей следующей таблицы из примера nextable. Затем пишем цикл:

for char in MyABCIterable(): print(char)

Оно работает! Итак, хотя документация Python говорит, что метод __iter__ должен возвращать итератор (реализуя как __iter__ , так и __next__ ), цикл for этого не требует.

Однако такая установка хрупкая. Возвращаясь к нашему правильно реализованному итератору, этот код будет работать:

my_abc = iter(MyABCIterator()) for char in my_abc: print(char)

А код с нашей комбинацией iterable + nextable — нет:

my_abc = iter(MyABCIterable()) for char in my_abc: print(char)

Мы получаем TypeError: 'MyABCNextable' object is not iterable . Вот поэтому протокол итератора определен так, как он определен. Это позволяет передавать итератор и по-прежнему перебирать его. Например, как если бы вы сперва создали итератор, а затем передали его в другую функцию. В таком случае наш хак не сработал бы.

Заключение

Давайте подведем итоги! Во-первых, теперь вы знаете, что все циклы for в Python используют итераторы! Кроме того, как мы увидели, итераторы в Python позволяют нам отделить код, выполняющий итерацию, от кода, работающего с каждым элементом. Мы также надеемся, что вы узнали немного больше о том, как работают итераторы в Python и что такое протокол итератора.

Последний вывод, к которому мы пришли, немного более абстрактный и не связан конкретно с итераторами или даже с Python. Может показаться, что код работает хорошо, пока вы не наткнетесь на случай, когда он сломается. Просто попробовав комбинацию iterable + nextable в цикле for , мы бы не обнаружили, что код ломается при передаче итератора.

На этом пока все, и мы надеемся, вам понравился более глубокий взгляд на протокол итераторов в Python!

Источник

Оцените статью