Итераторы — Python: Списки
На предыдущем уроке мы рассмотрели цикл for и термин «итерирование». И если в других языках это слово могут применять к любым циклам, то в Python у этого слова есть и другое значение. Еще итерирование — это взаимодействие с неким объектом, поддерживающим протокол итерации.
Для начала разберем, что же такое протокол в контексте Python. Протоколом называют набор определенных действий над объектом.
Если некий объект А позволяет совершать над собой действия, описанные неким протоколом Б, то говорят: «объект А реализует протокол Б» или «объект А поддерживает протокол Б».
В последующих курсах вы узнаете, что различных протоколов в Python — множество.
Даже многие синтаксические конструкции языка работают для самых разных объектов сходным образом именно потому, что объекты реализуют специальные протоколы.
Так мы можем в шаблон подставлять не только строки, но и значения других типов, потому что эти типы реализуют протокол приведения к строке. В Python протоколы встречаются на каждом шагу.
Протокол итерации
Протокол итерации — один из самых важных протоколов в Python. Ведь именно он позволяет циклу for работать с самыми разными коллекциями единообразно.
В чем же заключается этот протокол? Протокол требует от объекта быть итерируемым — то есть иметь специальный метод __iter__ .
Если у итерируемого объекта вызвать метод __iter__ , то метод должен вернуть новый специальный объект — так называемый итератор. В свою очередь, итератор должен иметь метод __next__ .
Звучит сложно, но давайте рассмотрим живой пример — итерирование списка. Список — итерируемый, поэтому нам подходит. Итак, создадим список и итератор для него:
l = [1, 2, 3, 5, 8, 11] i = iter(l) print(i) # =>
Мы вызвали для списка функцию iter , но на самом деле эта функция просто вызывает у списка соответствующий метод __iter__ .
Это сделано для удобства чтения кода, ведь читать имена вроде __foo__ не очень удобно. Некоторые другие функции делают что-то подобное, например функция len .
Большинство специальных методов с похожими именами вызывается внутри каких-то языковых конструкций и не предназначено для вызова напрямую.
Теперь у нас есть итератор i . Попробуем вызвать у него метод __next__ как напрямую, так и с помощью более удобной функции next :
i.__next__() # 1 i.__next__() # 2 next(i) # 3 next(i) # 5
Как мы видим, при каждом вызове метод возвращает очередной элемент исходного списка. Между вызовами он помнит свою позицию в списке. Так итератор выполняет роль курсора в вашем редакторе текста: если нажимать стрелки, то курсор перемещается и указывает на новое место в тексте. Только итератор — это курсор, умеющий перемещаться только в одну сторону.
Но что же произойдет, когда элементы в списке кончатся? Проверим:
next(i) # 8 next(i) # 11 next(i) # Traceback (most recent call last): # File "", line 1, in # StopIteration
Когда итератор достиг конца исходного списка, последующий вызов next привел к специальной ошибке StopIteration . Только в этом случае это не ошибка, ведь все когда-нибудь заканчивается.
StopIteration — это исключение. Об исключениях мы поговорим позже. А пока нужно лишь знать, что те средства языка, которые работают на основе протокола итерации, умеют реагировать на это конкретное исключение. Например, цикл for молча завершает работу.
Теперь вы уже можете представить, как на самом деле работает цикл for . Он получает у итерируемого объекта новый итератор. Затем вызывает у итератора метод __next__ до тех пор, пока не будет выброшено исключение StopIteration .
Цикл for и итераторы
Что же будет, если сначала получить итератор, а потом передать его циклу for ? Такое возможно, ведь цикл for достаточно умен. Он понимает, что можно сразу начать вызывать __next__ .
Давайте напишем функцию, ищущую в цикле первую строку, длина которой больше пяти символов:
def search_long_string(source): for item in source: if len(item) >= 5: return item
А теперь создадим список, содержащий несколько подходящих строк, и запустим функцию для этого списка пару раз:
animals = ['cat', 'mole', 'tiger', 'lion', 'camel'] search_long_string(animals) # 'tiger' search_long_string(animals) # 'tiger'
Функция дважды вернула одну и ту же строку, ведь мы передали в нее iterable, а значит цикл for создавал каждый раз новый итератор.
Создадим итератор сами и передадим в функцию уже его:
animals = ['cat', 'mole', 'tiger', 'lion', 'camel'] cursor = iter(animals) search_long_string(cursor) # 'tiger' search_long_string(cursor) # 'camel' search_long_string(cursor) search_long_string(cursor)
Итератор запомнил состояние между вызовами функций, и мы нашли оба длинных слова. Последующие вызовы функции вернули None , потому что итератор дошел до конца и запомнил это.
А ведь итераторов для одного и того же списка можно создать несколько, и каждый будет помнить свою позицию. Работая с кодом на Python, вы непременно увидите интересные применения протокола итерации.
Генераторы
В Python итерируемыми считаются не только коллекции. Еще существуют генераторы. Элементы генератора не хранятся в нем, но создаются по мере необходимости. Для примера возьмем генератор range . Вот как он работает:
numbers = range(3, 11, 2) for n in numbers: print(n) # => 3 # => 5 # => 7 # => 9 list(numbers) # [3, 5, 7, 9]
Здесь range генерирует последовательность чисел от 3 до 10 с шагом 2 . Шаг и начальное значение можно опускать, тогда счет будет производиться от нуля и с шагом в единицу.
Цикл for итерирует числа. Затем используем функцию list , чтобы получить список — эта функция может принять в качестве единственного аргумента итерируемый объект или итератор, элементы которого сложит во вновь созданный список.
При этом функция list накапливает значения в список, а tuple — в кортеж.
Отметим, что range представляет собой перезапускаемый генератор. Для такого генератора можно создавать сколько угодно итераторов, и для каждого из них значения будут генерироваться заново.
Существуют и не перезапускаемые генераторы. Эти при вызове метода __iter__ всегда возвращают один и тот же итератор. Поэтому по значениям такого генератора можно пройтись только один раз.
Примером такого генератора является enumerate , который мы рассматривали на прошлом уроке. Давайте еще раз взглянем на него:
l = enumerate("asdf") list(l) # [(0, 'a'), (1, 's'), (2, 'd'), (3, 'f')] list(l) # []
Вторая попытка проитерировать объект в переменной l ничего не дает, потому что генератор уже отработал один проход.
А вот еще один встроенный генератор — zip . Этот генератор принимает на входе несколько итерируемых объектов или итераторов и поэлементно группирует в кортежи:
keys = ["foo", "bar", "baz"] values = [1, 2, 3, 4] for k, v in zip(keys, values): print(k, " ">, v) # => foo = 1 # => bar = 2 # => baz = 3 z = zip(range(10), "hello", [True, False]) list(z) # [(0, 'h', True), (1, 'e', False)] list(z) # []
Пример демонстрирует два момента:
- zip — не перезапускаемый
- zip — перестает генерировать кортежи, как только заканчиваются элементы в любом из источников
Генераторы и ленивые вычисления
Большая часть языков программирования выполняет код в том порядке, в котором элементы кода написаны:
- Инструкции выполняются сверху вниз
- Выражения вычисляются после того, как будут вычислены их составляющие
- Функции вызываются после того, как будут вычислены их аргументы
Такая модель исполнения называется энергичной.
Существует и ленивая модель вычисления. В рамках этой модели вычисления производятся только тогда, когда их результат становится действительно нужен.
В любой программе при разных входных данных могут быть не нужны отдельные вычисления. Поэтому ленивая модель вычисления может дать определенные преимущества: то, что не нужно, не будет вычислено. Таким образом ленивость можно рассматривать как своего рода оптимизацию.
Python — это язык с энергичной моделью вычисления, поэтому практически всегда и все вычисляет сразу. Однако отдельные элементы ленивости присутствуют и в Python.
Генераторы — один из таких элементов. Генераторы производят элементы только по мере необходимости. И даже целые конструкции, собранные из генераторов — эдакие конвейеры, которые собирают составные значения и производят сборку по одному изделию за раз.
Так составной генератор zip(range(100000000), «abc») не генерирует все сто миллионов чисел, ведь строка «abc» слишком коротка, чтобы образовать столько пар. Но даже и этих пар не будет, если результат вычисления этого выражения не будет проитерирован.
Так ленивость позволяет экономить память при обработке больших потоков данных — нам не нужно загружать все данные целиком, достаточно загружать и обрабатывать их небольшими порциями.
Открыть доступ
Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно
- 130 курсов, 2000+ часов теории
- 1000 практических заданий в браузере
- 360 000 студентов
Наши выпускники работают в компаниях:
Итератор на Python
Итератор — это поведенческий паттерн, позволяющий последовательно обходить сложную коллекцию, без раскрытия деталей её реализации.
Благодаря Итератору, клиент может обходить разные коллекции одним и тем же способом, используя единый интерфейс итераторов.
Сложность:
Популярность:
Применимость: Паттерн можно часто встретить в Python-коде, особенно в программах, работающих с разными типами коллекций, и где требуется обход разных сущностей.
Признаки применения паттерна: Итератор легко определить по методам навигации (например, получения следующего/предыдущего элемента и т. д.). Код использующий итератор зачастую вообще не имеет ссылок на коллекцию, с которой работает итератор. Итератор либо принимает коллекцию в параметрах конструктора при создании, либо возвращается самой коллекцией.
Концептуальный пример
Этот пример показывает структуру паттерна Итератор, а именно — из каких классов он состоит, какие роли эти классы выполняют и как они взаимодействуют друг с другом.
main.py: Пример структуры паттерна
from __future__ import annotations from collections.abc import Iterable, Iterator from typing import Any, List """ Для создания итератора в Python есть два абстрактных класса из встроенного модуля collections - Iterable, Iterator. Нужно реализовать метод __iter__() в итерируемом объекте (списке), а метод __next__() в итераторе. """ class AlphabeticalOrderIterator(Iterator): """ Конкретные Итераторы реализуют различные алгоритмы обхода. Эти классы постоянно хранят текущее положение обхода. """ """ Атрибут _position хранит текущее положение обхода. У итератора может быть множество других полей для хранения состояния итерации, особенно когда он должен работать с определённым типом коллекции. """ _position: int = None """ Этот атрибут указывает направление обхода. """ _reverse: bool = False def __init__(self, collection: WordsCollection, reverse: bool = False) -> None: self._collection = collection self._reverse = reverse self._position = -1 if reverse else 0 def __next__(self): """ Метод __next __() должен вернуть следующий элемент в последовательности. При достижении конца коллекции и в последующих вызовах должно вызываться исключение StopIteration. """ try: value = self._collection[self._position] self._position += -1 if self._reverse else 1 except IndexError: raise StopIteration() return value class WordsCollection(Iterable): """ Конкретные Коллекции предоставляют один или несколько методов для получения новых экземпляров итератора, совместимых с классом коллекции. """ def __init__(self, collection: List[Any] = []) -> None: self._collection = collection def __iter__(self) -> AlphabeticalOrderIterator: """ Метод __iter__() возвращает объект итератора, по умолчанию мы возвращаем итератор с сортировкой по возрастанию. """ return AlphabeticalOrderIterator(self._collection) def get_reverse_iterator(self) -> AlphabeticalOrderIterator: return AlphabeticalOrderIterator(self._collection, True) def add_item(self, item: Any): self._collection.append(item) if __name__ == "__main__": # Клиентский код может знать или не знать о Конкретном Итераторе или классах # Коллекций, в зависимости от уровня косвенности, который вы хотите # сохранить в своей программе. collection = WordsCollection() collection.add_item("First") collection.add_item("Second") collection.add_item("Third") print("Straight traversal:") print("\n".join(collection)) print("") print("Reverse traversal:") print("\n".join(collection.get_reverse_iterator()), end="")
Output.txt: Результат выполнения
Straight traversal: First Second Third Reverse traversal: Third Second First
Python. Урок 15. Итераторы и генераторы
Генераторы и итераторы представляют собой инструменты, которые, как правило, используются для поточной обработки данных. В уроке рассмотрим концепцию итераторов в Python, научимся создавать свои итераторы и разберемся как работать с генераторами.
Итераторы в языке Python
Во многих современных языках программирования используют такие сущности как итераторы. Основное их назначение – это упрощение навигации по элементам объекта, который, как правило, представляет собой некоторую коллекцию (список, словарь и т.п.). Язык Python, в этом случае, не исключение и в нем тоже есть поддержка итераторов. Итератор представляет собой объект перечислитель, который для данного объекта выдает следующий элемент, либо бросает исключение, если элементов больше нет.
Основное место использования итераторов – это цикл for. Если вы перебираете элементы в некотором списке или символы в строке с помощью цикла for, то ,фактически, это означает, что при каждой итерации цикла происходит обращение к итератору, содержащемуся в строке/списке, с требованием выдать следующий элемент, если элементов в объекте больше нет, то итератор генерирует исключение, обрабатываемое в рамках цикла for незаметно для пользователя.
Приведем несколько примеров, которые помогут лучше понять эту концепцию. Для начала выведем элементы произвольного списка на экран.
>>> num_list = [1, 2, 3, 4, 5] >>> for i in num_list: print(i) 1 2 3 4 5
Как уже было сказано, объекты, элементы которых можно перебирать в цикле for, содержат в себе объект итератор, для того, чтобы его получить необходимо использовать функцию iter(), а для извлечения следующего элемента из итератора – функцию next().
>>> itr = iter(num_list) >>> print(next(itr)) 1 >>> print(next(itr)) 2 >>> print(next(itr)) 3 >>> print(next(itr)) 4 >>> print(next(itr)) 5 >>> print(next(itr)) Traceback (most recent call last): File "", line 1, in module> print(next(itr)) StopIteration
Как видно из приведенного выше примера вызов функции next(itr) каждый раз возвращает следующий элемент из списка, а когда эти элементы заканчиваются, генерируется исключение StopIteration.
Создание собственных итераторов
Если нужно обойти элементы внутри объекта вашего собственного класса, необходимо построить свой итератор. Создадим класс, объект которого будет итератором, выдающим определенное количество единиц, которое пользователь задает при создании объекта. Такой класс будет содержать конструктор, принимающий на вход количество единиц и метод __next__(), без него экземпляры данного класса не будут итераторами.
class SimpleIterator: def __init__(self, limit): self.limit = limit self.counter = 0 def __next__(self): if self.counter self.limit: self.counter += 1 return 1 else: raise StopIteration s_iter1 = SimpleIterator(3) print(next(s_iter1)) print(next(s_iter1)) print(next(s_iter1)) print(next(s_iter1))
В нашем примере при четвертом вызове функции next() будет выброшено исключение StopIteration. Если мы хотим, чтобы с данным объектом можно было работать в цикле for, то в класс SimpleIterator нужно добавить метод __iter__(), который возвращает итератор, в данном случае этот метод должен возвращать self.
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: self.counter += 1 return 1 else: raise StopIteration s_iter2 = SimpleIterator(5) for i in s_iter2: print(i)
Генераторы
Генераторы позволяют значительно упростить работу по конструированию итераторов. В предыдущих примерах, для построения итератора и работы с ним, мы создавали отдельный класс. Генератор – это функция, которая будучи вызванной в функции next() возвращает следующий объект согласно алгоритму ее работы. Вместо ключевого слова return в генераторе используется yield. Проще всего работу генератор посмотреть на примере. Напишем функцию, которая генерирует необходимое нам количество единиц.
def simple_generator(val): while val > 0: val -= 1 yield 1 gen_iter = simple_generator(5) print(next(gen_iter)) print(next(gen_iter)) print(next(gen_iter)) print(next(gen_iter)) print(next(gen_iter)) print(next(gen_iter))
Данная функция будет работать точно также, как класс SimpleIterator из предыдущего примера.
Ключевым моментом для понимания работы генераторов является то, при вызове yield функция не прекращает свою работу, а “замораживается” до очередной итерации, запускаемой функцией next(). Если вы в своем генераторе, где-то используете ключевое слово return, то дойдя до этого места будет выброшено исключение StopIteration, а если после ключевого слова return поместить какую-либо информацию, то она будет добавлена к описанию StopIteration.
P.S.
Если вам интересна тема анализа данных, то мы рекомендуем ознакомиться с библиотекой Pandas. На нашем сайте вы можете найти вводные уроки по этой теме. Все уроки по библиотеке Pandas собраны в книге “Pandas. Работа с данными”.
Раздел: Python Уроки по Python Метки: Python, Уроки Python
Python. Урок 15. Итераторы и генераторы : 8 комментариев
- Даниил 25.05.2018 У вас ошибка в коде: где описывается первый SimpleIterator, метод __next__ должен возвращать self.counter вместо 1
- worker 30.05.2018 Спасибо за внимательность, но там на самом деле по замыслу и должна стоять единица))). Это пример наипростейшего итератора, он возвращает единицу self.limit раз. Но и ваш вариант тоже верен!
Итераторы#
Итератор (iterator) — это объект, который возвращает свои элементы по одному за раз.
С точки зрения Python — это любой объект, у которого есть метод __next__ . Этот метод возвращает следующий элемент, если он есть, или возвращает исключение StopIteration, когда элементы закончились.
Кроме того, итератор запоминает, на каком объекте он остановился в последнюю итерацию.
В Python у каждого итератора присутствует метод __iter__ — то есть, любой итератор является итерируемым объектом. Этот метод просто возвращает сам итератор.
Пример создания итератора из списка:
In [3]: numbers = [1, 2, 3] In [4]: i = iter(numbers)
Теперь можно использовать функцию next(), которая вызывает метод __next__ , чтобы взять следующий элемент:
In [5]: next(i) Out[5]: 1 In [6]: next(i) Out[6]: 2 In [7]: next(i) Out[7]: 3 In [8]: next(i) ------------------------------------------------------------ StopIteration Traceback (most recent call last) ipython-input-8-bed2471d02c1> in module>() ----> 1 next(i) StopIteration:
После того, как элементы закончились, возвращается исключение StopIteration.
Для того, чтобы итератор снова начал возвращать элементы, его надо заново создать.
Аналогичные действия выполняются, когда цикл for проходится по списку:
In [9]: for item in numbers: . : print(item) . : 1 2 3
Когда мы перебираем элементы списка, к списку сначала применяется функция iter(), чтобы создать итератор, а затем вызывается его метод __next__ до тех пор, пока не возникнет исключение StopIteration.
Итераторы полезны тем, что они отдают элементы по одному. Например, при работе с файлом это полезно тем, что в памяти будет находиться не весь файл, а только одна строка файла.
Файл как итератор#
Один из самых распространенных примеров итератора — файл.
! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
Если открыть файл обычной функцией open, мы получим объект, который представляет файл:
In [10]: f = open('r1.txt')
Этот объект является итератором, что можно проверить, вызвав метод __next__ :
In [11]: f.__next__() Out[11]: '!\n' In [12]: f.__next__() Out[12]: 'service timestamps debug datetime msec localtime show-timezone year\n'
Аналогичным образом можно перебирать строки в цикле for:
In [13]: for line in f: . print(line.rstrip()) . service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !
При работе с файлами, использование файла как итератора не просто позволяет перебирать файл построчно — в каждую итерацию загружена только одна строка. Это очень важно при работе с большими файлами на тысячи и сотни тысяч строк, например, с лог-файлами.
Поэтому при работе с файлами в Python чаще всего используется конструкция вида:
In [14]: with open('r1.txt') as f: . for line in f: . print(line.rstrip()) . ! service timestamps debug datetime msec localtime show-timezone year service timestamps log datetime msec localtime show-timezone year service password-encryption service sequence-numbers ! no ip domain lookup ! ip ssh version 2 !