Код
#статьи

Генераторы Python: что это такое и зачем они нужны

Генераторы используют, чтобы оперативная память не давилась большими объёмами информации. В 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), ничего считаться не будет. Чтобы запустить генератор ещё раз, придётся создавать его заново.

И что, для вычисления генератора придётся много раз вызывать next()?

Нет, значения можно вычислять в цикле 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 для анализа потоков данных в корпоративной многозадачности. Генераторы позволяют создавать сложные разветвлённые программы для обработки потоков.

Что ещё можно сказать

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

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

Нейросети для работы и творчества!
Хотите разобраться, как их использовать? Смотрите конференцию: четыре топ-эксперта, кейсы и практика. Онлайн, бесплатно. Кликните для подробностей.
Смотреть программу
Понравилась статья?
Да

Пользуясь нашим сайтом, вы соглашаетесь с тем, что мы используем cookies 🍪

Ссылка скопирована