Главная / Курсы / Python / Итераторы и генераторы
# Глава 23. Итераторы и генераторы > Работа, ты меня не бойся, я тебя не трону! Народная пословица Итераторы позволяют поштучно перебирать последовательности объектов. Генераторы — это итераторы, реализующие стратегию ленивых вычислений. Они помогают сэкономить оперативную память при переборе больших (и даже бесконечных!) последовательностей. Разберемся, как их применять на практике. ## Итераторы и итерабельные объекты Итерабельным объектом в питоне называется объект, который способен возвращать элементы по одному. Из него можно получить итератор — специальный объект для перемещения по элементам итерабельного объекта. Два классических примера использования итераторов и итерабельных объектов — цикл `for` и выражение `in`: ```python {.example_for_playground} numbers = [1, 2, 3] for n in numbers: print(n) if 2 in numbers: print("found item") ``` ``` 1 2 3 found item ``` В данном примере итерабельным объектом является список `numbers`. Чтобы пройтись по нему, под капотом цикла `for` создается итератор. Он и перебирает элементы, пока они не кончатся. Если немного упростить, то цикл `for n in numbers` — всего лишь синтаксический сахар примерно над такой конструкцией: ```python def for_loop(numbers): iterator = numbers.__iter__() while True: try: n = iterator.__next__() except StopIteration: break else: print(n) ``` На вход цикла `for` должен поступать итерабельный объект. То есть такой объект, у которого присутствует dunder-метод `__iter__()`, возвращающий итератор. Для начала работы с итерабельным объектом необходимо получить итератор на первый элемент. Благодаря ему можно забирать элементы коллекции и двигаться по ней вперед. При перемещении по коллекции у итератора вызывается dunder-метод `__next__()`, который возвращает следующий элемент. Если следующего элемента нет, метод бросает исключение `StopIteration`. Генерация этого исключения не считается ошибочной ситуацией: `StopIteration` выступает в качестве маркера окончания прохода по коллекции для вызывающего кода. Исключение `StopIteration` сигнализирует, что элементы иссякли и итерироваться дальше не нужно. Итератор — объект, который позволяет поэлементно обходить коллекции: списки, кортежи, словари и так далее. Он отвечает за: - возвращение данных по одному элементу из контейнера или потока, - отслеживание текущего и пройденных элементов. Объект считается итератором, если он **следует протоколу итератора,** то есть определяет два dunder-метода: - `__iter__()` — возвращает инстанс итератора. Чтобы не вызывать этот dunder-метод напрямую, в языке реализована встроенная функция-обертка `iter()`. - `__next__()` — возвращается элемент, на который указывает итератор. Метод должен быть заточен под тип контейнера. При окончании итерации `__next__()` должен бросить исключение `StopIteration`. Для этого метода существует обертка в виде встроенной функции `next()`. В промышленном коде почти никогда не требуется напрямую работать с dunder-методами итератора: они неявно вызываются в циклах. Рассмотрим пример класса, следующего протоколу итератора: ```python class SeqIterator: def __init__(self, sequence): self._sequence = sequence self._index = 0 def __iter__(self): return self def __next__(self): if self._index < len(self._sequence): item = self._sequence[self._index] self._index += 1 return item raise StopIteration ``` Если объект является итератором, то он автоматически считается итерабельным объектом. Обратное в общем случае неверно. Например, итератор `range()` — это итерабельный объект. А итерабельный объект типа `tuple` итератором точно не является. По мере перебора итератор истощается, и для повторного прохода по элементам его нужно пересоздавать. Зато он занимает очень мало места в памяти. А итерабельный объект никогда не истощается, но занимает в памяти столько места, чтобы уместить в нем все свои элементы. Возьмем нашу реализацию итератора `SeqIterator` из примера выше и создадим с ее помощью итерабельный класс `SeqIterable`. ```python {.example_for_playground} class SeqIterable: def __init__(self, sequence): self._sequence = sequence def __iter__(self): return SeqIterator(self._sequence) for item in SeqIterable([1, 2, 3]): print(item) ``` ``` 1 2 3 ``` Что выведет этот код? {.task_text} В случае исключения напишите `error`. {.task_text} ```python {.example_for_playground} vals = iter([1, 2, 3]) a = 2 in vals b = 2 in vals print(a is b) ``` ```consoleoutput {.task_source #python_chapter_0230_task_0010} ``` Встроенная функция `iter()` вызвала dunder-метод `__iter__()` у списка. В переменную `vals` сохранился итератор. Затем выражение `in` переместило итератор по коллекции до элемента 2. Многократный проход по одним и тем же элементам через один и тот же итератор невозможен: для этого итератор нужно пересоздавать. Поэтому повторный вызов `in` вернул `False`. {.task_hint} ```python {.task_answer} False ``` Реализуйте класс-итератор `KVIterator`: обертку для итерирования по ключам и значениям словаря. {.task_text} ```python {.task_source #python_chapter_0230_task_0020} class KVIterator: def __init__(self, d): ... d = {"a": 1, "b": 2} for k, v in KVIterator(d): print(f"{k}={v}") ``` В инициализатор `KVIterator` должен передаваться словарь. Назовем его `d`. Тогда внутри инициализатора можно завести итератор: `self._i = iter(d.items())`. {.task_hint} ```python {.task_answer} class KVIterator: def __init__(self, d): self._i = iter(d.items()) def __iter__(self): return self._i def __next__(self): return next(self._i) ``` ## Генераторы {#block-generators} Генераторы — это итераторы специального вида. С их помощью реализуется стратегия [ленивых вычислений](https://en.wikipedia.org/wiki/Lazy_evaluation). Вычисления называются ленивыми, если они откладываются ровно до того момента, пока не потребуется их результат. Это и происходит в процессе итерирования по генератору. В названии генераторов заложена их суть: данные в генераторе не хранятся, а рассчитываются, «генерируются» в момент обращения. Для того чтобы функция считалась генератором, в ней должно присутствовать ключевое слово `yield`. Главное отличие генераторов от обычных функций в том, что для возврата значения вместо `return` используется оператор `yield`. Он не завершает выполнение функции, а только приостанавливает (yield означает «уступать»). Таким образом запоминается состояние, в котором функция была приостановлена. Под состоянием понимается набор локальных переменных, адрес для возобновления работы генератора и регистры общего назначения. Все это хранится в объекте генератора. При последующем обращении к генератору его состояние восстанавливается и вычисления возобновляются с того места, где были прерваны. При завершении работы генератора неявно вызывается оператор `return`. Рассмотрим пример генератора: ```python {.example_for_playground} def sequence_generator(sequence): for item in sequence: yield item for number in sequence_generator([1, 2, 3]): print(number) ``` ``` 1 2 3 ``` При вызове функции `sequence_generator()` наружу возвращается генератор. Выполнение функции при этом еще не началось! Чтобы его инициировать, нужно вызвать метод `__next__()` генератора. Внутри цикла `for` этот вызов происходит неявно. C помощью конструкции `yield from` удобно создавать генераторы-посредники. Рассмотрим редуцированный пример: ```python {.example_for_playground} def f(items): for item in items: yield item g = f('abcd') print(list(g)) ``` ``` ['a', 'b', 'c', 'd'] ``` В генератор `f()` передан итерабельный объект `items`. Генератор по одному возвращает из него элементы. `yield from` позволяет это выразить более компактно: ```python {.example_for_playground} def f(items): yield from items ``` В чем преимущество генераторов над обычными контейнерами? В каких случаях использовать их более выгодно? - Если неизвестен объем обрабатываемых данных. Например, при работе с потоком данных из сокета. - Если в каждый момент времени требуется работать с элементом какой-либо последовательности, но не с последовательностью целиком. Например, при построчной обработке большого файла. - При работе с длинными или бесконечными последовательностями. Например, обработка в цикле значений, полученных из генератора случайных чисел. Напишите генератор `even_sequence()`, который при каждом обращении генерирует положительные четные числа, начиная с 0. {.task_text} ```python {.task_source #python_chapter_0230_task_0030} ``` Внутри генератор заводится целочисленная переменная, инициализирующаяся нулем. В вечном цикле после каждого `yield` она инкрементируется на 2. {.task_hint} ```python {.task_answer} def even_sequence(): x = 0 while True: yield x x += 2 ``` Генератор — это подвид итератора. Поэтому при его истощении тоже выбрасывается исключение `StopIteration`: ```python {.example_for_playground} def strange_generator(): print("before return") return print("after return") yield g = strange_generator() next(g) ``` ``` before return Traceback (most recent call last): File "example.py", line 8, in <module> next(g) StopIteration ``` Так как внутри функции `strange_generator()` есть ключевое слово `yield`, она считается генератором. Генератор при создании абсолютно ничего не делает и ждет, пока его запустят, то есть вызовут метод `__next__()`. Как только мы запускаем генератор, функция выполняется до оператора `yield` либо `return`. При достижении `yield` функция приостанавливает свое выполнение и ждет возобновления. А при достижении `return` происходит завершение работы генератора: выбрасывается исключение `StopIteration`. ## Бесконечные генераторы В модуле `itertools` есть генератор `cycle`. Он позволяет закольцованно перебирать элементы переданного ему итерабельного объекта: ```python from itertools import cycle pool = cycle(["-", "\\", "|", "/"]) for i, elem in enumerate(pool): print(elem) if i > 5: break ``` ``` - \ | / - \ | ``` Напишите бесконечный генератор `cycle`, который по кругу выдает элементы переданного ему списка. {.task_text} ```python {.task_source #python_chapter_0230_task_0050} def cycle(lst): # Your code here pool = cycle(["A", "B", "C"]) for i, elem in enumerate(pool): print(elem) if i > 5: break ``` Организуйте два вложенных цикла: внутри `while True` циклом `for item in lst` перебирайте элементы `lst` и возвращайте их через `yield`. {.task_hint} ```python {.task_answer} def cycle(lst): while True: for item in lst: yield item pool = cycle(["A", "B", "C"]) for i, elem in enumerate(pool): print(elem) if i > 5: break ``` ## Встроенные функции для работы с генераторами В питоне есть ряд встроенных функций, принимающих или возвращающих генераторы. Например, функция `open()` может быть использована для открытия текстовых файлов. В таком случае она возвращает генератор, который позволяет лениво итерироваться по строкам в файле. По сравнению с загрузкой файла целиком такой подход существенно экономит память: ```python import sys with open("/path/to/file.txt") as f: print(sys.getsizeof(f)) for l in f: print(l) ``` ``` 208 Python is awesome ``` В данном примере мы вызвали `open()` через [контекстный менеджер,](/courses/python/chapters/python_chapter_0210/) получили генератор `f` и с его помощью проитерировались по строкам файла. Размер объекта генератора, рассчитанный через `getsizeof()`, не зависит от объема файла. Если бы в нашем файле было не три строки, а несколько миллионов, генератор бы по-прежнему занимал несколько байтов. Помимо `open()`, генераторы возвращают встроенные функции `enumerate()`, `range()`, `map()` и [другие.](/courses/python/chapters/python_chapter_0280/) Функции-конструкторы встроенных коллекций принимают на вход итераторы. В том числе в них можно передать и генератор: ```python {.example_for_playground} print(list(range(5))) print(dict(enumerate(range(8)))) ``` ``` [0, 1, 2, 3, 4] {0: 0, 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7} ``` Стандартная библиотека также предоставляет множество полезных генераторов, [среди которых](/courses/python/chapters/python_chapter_0280/) `itertools.chain()` и `itertools.zip_longest()`. ## Генератор с получаемым значением Помимо возрата значений из генератора существует возможность отправить в него значение. Генератор, который умеет получать значение, можно считать урезанной **корутиной**. Урезанной, потому что нельзя использовать в цикле событий asyncio. Подробнее о корутинах мы [поговорим](/courses/python/chapters/python_chapter_0320/) в главе про модуль asyncio. Рассмотрим пример генератора с получаемым значением. ```python {.example_for_playground} def input_gen(): while True: r = yield print(f"{r}") c = input_gen() c.send(None) # либо next(c) c.send("Hi, input_gen") next(c) ``` ``` Hi, input_gen None ``` Обратите внимание: после создания такого генератора его необходимо запустить либо через `send(None)`, либо через `next()`. Только после этого он будет готов принимать значения через `send()`. Если до вызова `next()` вызвать метод `send()` с аргументом, отличным от `None`, произойдет исключение: ``` TypeError: can't send non-None value to a just-started generator ``` В нашем примере в консоль вывелось две строки. Вызов метода `send()`, помимо отправки значения в генератор, запускает его до следующего `yield`. Поэтому при отправке `c.send("Hi, input_gen")` происходит прокрутка генератора (включая вывод в консоль переданного значения) до следующего `yield`. И при последующем вызове `next(c)` в консоль выводится переданное значение: `None`. Как мы уже выяснили, с помощью выражения `yield` можно и отправлять, и получать данные. Мало кто знает, но эти две операции легко **объединить:** ```python b = yield a ``` Как это читать? Сначала вернуть из корутины `a`, и когда какое-то значение будет отправлено в корутину через `send()`, присвоить его `b`. Таким образом через полученное значение `b` корутина может узнать, как вызывающая сторона отреагировала на передачу из корутины значения `a`. Для остановки генератора существует метод `close()`. Но явно он почти никогда не вызывается, вместо этого объект генератора уничтожается сборщиком мусора. Напишите функцию для поиска подстроки в тексте. Пусть она инициализируется искомой подстрокой и в вечном цикле принимает строки для поиска. Если подстрока найдена, пусть генератор отдает обратно индекс подстроки в строке. Если не найдена — то значение `-1`. {.task_text} ```python {.task_source #python_chapter_0230_task_0040} def search(substr): # Your code here finder = search("is") next(finder) assert finder.send("Now is better than never.") == 4 assert finder.send("Readability counts.") == -1 ``` Пусть генератор `search()` принимает аргумент `substr`. Тогда в генераторе можно завести переменную `i`, хранящую индекс подстроки `substr` в искомом тексте, и проинициализировать ее значением -1. В вечном цикле генератор будет возвращать `i`, принимать строку `line` для поиска и искать подстроку. Первые два действия объединяются конструкцией: `line = yield i`. Затем следует определение индекса подстроки в полученной строке: `i = line.find(substr)`. {.task_hint} ```python {.task_answer} def search(substr): i = -1 while True: line = yield i i = line.find(substr) finder = search("is") next(finder) assert finder.send("Now is better than never.") == 4 assert finder.send("Readability counts.") == -1 ``` ## Резюмируем - Итератор — объект, который позволяет поэлементно перебирать последовательности значений. - Объект считается итератором, если он реализует протокол итератора: dunder-методы `__iter__()` и `__next__()`. - При истощении генератора он должен бросить исключение `StopIteration`. - Генератор — это итератор для реализации ленивых вычислений. Например, он полезен для обработки бесконечных последовательностей. - С помощью выражения `yield` можно и отправлять, и получать данные из генератора. - В питоне есть множество встроенных генераторов для решения типичных задач: `open()`, `zip()`, `map()`, `range()` и другие.
Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!