Генераторы Python: что это такое и зачем они нужны
2026-02-21 01:02 Diff

#статьи

  • 26 янв 2021
  • 0

Генераторы используют, чтобы оперативная память не давилась большими объёмами информации. В Python это фишки, экономящие память.

 vlada_maestro / shutterstock

Программист, консультант, специалист по документированию. Легко и доступно рассказывает о сложных вещах в программировании и дизайне.

Допустим, у вас есть файл, который весит десяток гигабайт. Из него нужно выбрать и обработать строки, подходящие под какое-то условие, а то и сравнить со строками другого большого файла.

Другой пример: нужно проанализировать практически бесконечный поток данных. Это могут быть, например, показания счётчиков, биржевые котировки, сетевой трафик.

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

Что делать? Хранить такие объёмы данных в компьютере нереально: они не поместятся в оперативную память — а некоторые и на жёсткий диск. Выход один — обрабатывать информацию небольшими порциями, чтобы не вызывать переполнения памяти. В Python на этот случай есть специальный инструмент — генераторы.

  • Генератор — это объект, который сразу при создании не вычисляет значения всех своих элементов.
  • Он хранит в памяти только последний вычисленный элемент, правило перехода к следующему и условие, при котором выполнение прерывается.
  • Вычисление следующего значения происходит лишь при выполнении метода next(). Предыдущее значение при этом теряется.

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

Рассмотрим пример: создадим объект-генератор gen с помощью так называемого генераторного выражения. Он будет считать квадраты чисел от 1 до 4 — такую последовательность создаёт функция range(1,5).

>>> a = (i**2 for i in range(1,5)) >>> a <generator object <genexpr> at 0x0000023A7524D6D0> >>> next(a) 1 >>> next(a) 4 >>> next(a) 9 >>> next(a) 16 >>> next(a) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration

Когда мы выведем на консоль переменную gen, то увидим лишь сообщение, что это объект-генератор.

При четырёх вызовах метода next(a) будут по одному рассчитываться и выводиться на консоль значения генератора: 1, 4, 9, 16. Причём в памяти будет сохраняться только последнее значение, а предыдущие сотрутся.

Когда мы попытаемся вызвать next(gen) в пятый раз, генератор сотрёт из памяти последний элемент (число 16) и выдаст исключение StopIteration.

>>> next(gen) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration

Всё! Генератор больше не работает. Сколько бы мы ни вызывали next(gen), ничего считаться не будет. Чтобы запустить генератор ещё раз, придётся создавать его заново.

Нет, значения можно вычислять в цикле for. В этом случае метод next() вызывается неявно. Например:

>>> a = (i**2 for i in range(1,5)) >>> for i in a: ... print(i) 1 4 9 16

Когда весь цикл пройден, произойдёт исключение StopIteration. Хотя на консоль сообщение об этом не выводится, но генератор помнит о нём и больше работать не будет. То есть цикл for можно запускать только один раз, во второй раз не получится. Нельзя об этом забывать.

Для этого сначала рассмотрим упрощённый способ создания генератора — с помощью генераторного выражения.

Генераторные выражения позволяют создавать объект-генератор в одну строчку. В общем случае их пишут по шаблону:

(выражение for j in итерируемый объект if условие)

Где for, in, if — ключевые слова, j — переменная.

Пример генераторного выражения мы рассмотрели выше. Теперь посмотрим, как можно применить его для обработки большого файла.

Перед нами задача: на сервере есть огромный журнал событий log.txt, в котором хранятся сведения о работе какой-то системы за год. Из него нужно выбрать и обработать для статистики данные об ошибках — строки, содержащие слово error.

Такие строки можно выбрать и сохранить в памяти с помощью списка:

with open(path + "\log.txt", "r") as log_file: err_list = [st for st in log_file if "error" in st]

Здесь path — путь к файлу log. В результате сформируется список вида:

[строка1, строка2, строка3, ….]

В списке e_l содержатся все строки со словом error, они записаны в память компьютера. Теперь их можно обработать в цикле. Недостаток метода в том, что, если таких строк будет слишком много, они переполнят память и вызовут ошибку MemoryError.

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

Рассмотрим следующий код:

with open("path\log.txt", "r") as log_file: err_gen = (st for st in log_file if "error" in st) for item in err_gen: <обработка строки item>
  • Генераторное выражение возвращает объект-генератор err_gen.
  • Генератор начинает в цикле выбирать из файла по одной строке со словом error и передавать их на обработку.
  • Обработанная строка стирается из памяти, а следующая записывается и обрабатывается. И так до конца цикла.

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

Генераторы часто используют при веб-скрапинге. Они позволяют поочерёдно получать нужные веб-страницы и обрабатывать их информацию. Это намного эффективнее, чем загрузить в память сразу все выбранные страницы и затем обрабатывать их в цикле.

Генераторные выражения — это упрощённый вариант функций-генераторов, также создающих генераторы.

Функция-генератор отличается от обычной функции тем, что вместо команды return в ней используется yield. И если return завершает работу функции, то инструкция yield лишь приостанавливает её, при этом она возвращает какое-то значение.

При первом вызове метода next() выполняется код функции с первой команды до yield. При втором next() и последующих до конца генератора — код со следующей после yield команды и до тех пор, пока yield не встретится снова.

Чтобы было понятнее, рассмотрим небольшой пример:

>>> def f_gen(m): ... s = 1 ... for n in range(1,m): ... yield n**2 + s ... s += 1 ... >>> a = f_gen(5) >>> a <generator object f_gen at 0x0000023EE468D6D0> >>> for i in a: ... print(i) ... 2 6 12 20 >>>

Здесь функция f_gen(5) при вызове создаёт генератор a. Мы видим это, когда выводим a на консоль.

Посчитаем значения генератора в цикле for.

  • При первой итерации выполняется код функции до yield: переменная s =1, n = 1, yield возвращает 2.
  • При второй итерации выполняется оператор после yield, далее к началу цикла и опять до yield: s = 2, n = 2, yield возвращает 6.
  • Соответственно, при третьей и четвёртой итерации генерируются значения 12 и 20, после чего выполнение генератора прекращается.

Как видим, значения переменных n и s между вызовами сохраняются.

Yield — инструмент очень гибкий. Его можно несколько раз использовать в коде функции-генератора. В этом случае команды yield служат разделителями кода: при первом вызове метода next() выполняется код до первого yield, при следующих вызовах — операторы между yield. При этом в генераторной функции необязательно должен быть цикл, все значения генератора и так посчитаются.

Рассмотрим, как можно с помощью генератора создать математическую последовательность, например, программу, генерирующую простые числа (напоминаем, это числа, не имеющие делителей, кроме 1).

Наша программа будет последовательно анализировать целые числа больше 1. Для каждого числа n программа ищет делители в диапазоне от 2 до √n. Если делители есть, программа переходит к следующему числу. Если их нет, значит, n — число простое, и программа выводит его на печать.

>>> import math >>> def prime_num(): ... nm = 2 ... while True: ... sq = math.ceil(nm**1/2) ... for i in range(2, sq+1): ... if (nm % i) == 0: ... break ... else: ... yield nm ... nm += 1 ... >>> for num in prime_num(): ... print(num) ... 2 3 5 7 11 13 17 19 23 29 31

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

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

Когда-то был один next (), но в Python 2.5 появилось ещё три метода:

  • .close () — останавливает выполнение генератора;
  • .throw () — генератор бросает исключение;
  • .send () — интересный метод, позволяет отправлять значения генератору.

Рассмотрим пару небольших примеров.

Сначала на .close () и .throw ():

>>> def f_gen(): ... n = 1 ... while True: ... yield n**2 ... n += 1 ... >>> generator1 = f_gen() >>> generator2 = f_gen() >>> >>> for i in generator1: ... print(i) ... if i > 10: ... generator1.close() ... 1 4 9 16 >>> for i in generator2: ... print(i) ... if i > 20: ... generator2.throw(Exception("Плохо!")) ... 1 4 9 16 25 Traceback (most recent call last): File "<stdin>", line 4, in <module> File "<stdin>", line 4, in f_gen Exception: Плохо!

Программа создаёт два генератора, возвращающих бесконечную последовательность квадратов чисел. Их выполнение прекращается с помощью методов .close() и .throw().

Пример использования .send()

>>> def generator(x): ... while True: ... x = yield x + 1 ... >>> g = generator(5) >>> g.send(None) 6 >>> g.send(10) 11 >>> g.send(15) 16 >>> g.send(4) 5

Здесь мы не получаем значения генератора, а отправляем их на обработку с помощью метода .send().

С помощью этих методов можно создавать сопрограммы, или корутины, — это функции, которым можно передавать значения, приостанавливать и снова возобновлять их работу. Их обычно используют в Python для анализа потоков данных в корпоративной многозадачности. Генераторы позволяют создавать сложные разветвлённые программы для обработки потоков.

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

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

Python для всех

Вы освоите Python на практике и создадите проекты для портфолио — телеграм-бот, веб-парсер и сайт с нуля. А ещё получите готовый план выхода на удалёнку и фриланс. Спикер — руководитель отдела разработки в «Сбере».

Пройти бесплатно

Бесплатный курс по разработке на Python ➞
Пройдите бесплатный курс по Python и создайте с нуля телеграм-бот, веб-парсер и сайт. Спикер — руководитель отдела разработки в «Сбере». Пройти курс