что это такое и зачем они нужны / Skillbox Media
#статьи
- 11
Генераторы используют, чтобы оперативная память не давилась большими объёмами информации. В Python это фишки, экономящие память.
Vkontakte Twitter Telegram Скопировать ссылкуМарина Демидова
Программист, консультант, специалист по документированию. Легко и доступно рассказывает о сложных вещах в программировании и дизайне.
Допустим, у вас есть файл, который весит десяток гигабайт. Из него нужно выбрать и обработать строки, подходящие под какое-то условие, а то и сравнить со строками другого большого файла.
Другой пример: нужно проанализировать практически бесконечный поток данных. Это могут быть, например, показания счётчиков, биржевые котировки, сетевой трафик.
А может, нужно создать поток данных самостоятельно: рассчитать комбинаторную структуру для определения вероятности какого-то события, математическую последовательность или последовательность случайных чисел.
Что делать? Хранить такие объёмы данных в компьютере нереально: они не поместятся в оперативную память — а некоторые и на жёсткий диск. Выход один — обрабатывать информацию небольшими порциями, чтобы не вызывать переполнения памяти. В 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, далее к началу цикла и опять до
- Соответственно, при третьей и четвёртой итерации генерируются значения 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 для анализа потоков данных в корпоративной многозадачности. Генераторы позволяют создавать сложные разветвлённые программы для обработки потоков.
С изучения генераторов начинается освоение последовательной обработки гигантских потоков данных. Это может быть, например, трейдинг и технический анализ в биржевых операциях.
Но даже если не говорить о глобальных задачах, скрипты с применением генераторов — это способ избежать копирования данных в память. Генераторы позволяют экономить ресурсы компьютера и создавать красивый чистый код.
Читайте также:
Vkontakte Twitter Telegram Скопировать ссылку
Учись бесплатно:
вебинары по программированию, маркетингу и дизайну.
Участвовать
«Яндекс» выпустил расширенную версию корпоративного браузера 03 мар 2023
Вышла криптоальтернатива Twitter под названием Bluesky 01 мар 2023
Meta* представила свою нейросеть для исследователей в области ИИ 28 фев 2023
Понравилась статья?
Да
Генераторы в Python. Оператор yield. Урок 11 курса «Объектно-ориентированное программирование на Python»
Генераторы можно считать подвидом итераторов, а способ их создания – инструментом для создания несложных итераторов.
В отличие от обычных итераторов, генераторы создаются путем вызова функции, а не от класса.
Чтобы функция возвращала объект-генератор, в ее теле должен быть оператор yield. Когда любая yield-содержащая функция вызывается, она возвращает объект типа generator, а не None или какой-нибудь другой тип данных через оператор return.
У генераторов методы __next__() и __iter__() создаются средствами самого языка, то есть автоматически. Программисту их определять не надо, что упрощает создание пользовательских типов итераторов.
>>> def starmaker(n): ... while n > 0: ... yield '*' ... n -= 1 ... >>> type(starmaker) >class 'function'> >>> s = starmaker(3) >>> type(s) >class 'generator'> >>> next(s) '*' >>> next(s) '*' >>> next(s) '*' >>> next(s) Traceback (most recent call last): File ">stdin>", line 1, in >module> StopIteration
В определенном смысле оператор yield заменяет return с тем исключением, что мы снова возвращаемся в функцию, когда вызывается next(). При этом объект-генератор помнит состояние переменных и место, откуда при прошлом вызове произошел выход из функции.
Если мы сделаем нечто подобное
>>> def g(): ... yield 1 ...
то не получим бесконечный генератор, потому что код тела функции полностью выполнится при первом вызове next():
>>> a = g() >>> next(a) 1 >>> next(a) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
Обратите внимание, что функция starmaker() делает то же самое, что класс, описанный в прошлом уроке:
>>> class A: ... def __init__(self, qty): ... self.qty = qty ... def __iter__(self): ... return self ... def __next__(self): ... if self.qty > 0: ... self.qty -= 1 ... return '+' ... else: ... raise StopIteration ... >>> a = A(3) >>> for i in a: ... print(i) ... + + +
При этом код функции, создающей итератор, намного короче аналогичного класса. Поэтому классы-итераторы скорее уместны, когда создаются сложные объекты, включающие множество полей и сложную логику их обработки, а не только методы __iter__() и __next__().
Генераторные выражения
Существует еще более простой, чем функция с yield, способ создания итераторов – генераторные выражения. Они подходят, когда код тела функции можно записать в одно выражение.
Синтаксис генераторных выражений подобен генераторам списков, рассматриваемых в курсе «Python. Введение в программирование». Однако, в отличие от списков, в случае генераторов используются круглые скобки.
Напомним, как выглядят генераторы списков и то, что возвращают они списковый тип данных:
>>> a = [i+1 for i in range(10)] >>> a [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] >>> type(a) <class 'list'> >>> import random >>> b = [random.randint(0,9) for i in range(5)] >>> b [2, 5, 5, 2, 9] >>> c = [i for i in b if i % 2 == 0] >>> c [2, 2]
Результат выражения, стоящего до for, добавляется на каждой итерации цикла в итоговый список. Выполнение выражения генератора списка сразу заполняет список.
В случае генераторных выражений создается объект-генератор, у которого будет вычисляться очередной элемент только при каждом вызове next():
>>> a = (i+1 for i in range(10)) >>> a <generator object <genexpr> at 0x7fa586339f10> >>> type(a) <class 'generator'> >>> next(a) 1 >>> next(a) 2
Пример со звездочкой с помощью генераторного выражения будет выглядеть так:
>>> d = ('*' for i in range(5)) >>> for i in d: ... print(i) ... * * * * *
В отличие от генераторных выражений, yield-функции более универсальны не только из-за произвольного количества кода в их теле. В них вы можете передавать разные значения аргументов. А значит, одна и та же функция может использоваться для создания несколько разных генераторов.
Практическая работа
В задании к прошлому уроку требовалось написать класс-итератор, объекты которого генерируют случайные числа в количестве и в диапазоне, которые передаются в конструктор. Напишите выполняющую ту же задачу генераторную функцию. В качестве аргументов она должна принимать количество элементов и диапазон.
Курс с примерами решений практических работ:
pdf-версия, android-приложение
генераторов в Python — Geeksforgeeks
Связанные статьи
- Написать опыт обследования
Напишите статью
ВВЕДЕНИЕ
Вход
Операторы
Типы данных
. Обработка исключений
Обработка файлов
Python Regex
Коллекции Python
Python Advance
Python NumPy
Python Pandas
Python Django
Python JSON
Python CSV
Python MySQL
Python MongoDB
Python OpenCV
Python Selenium
Python Tkinter
Python Kivy
Data Visualization
Python Examples and Quiz
Улучшить статью
Сохранить статью
- Уровень сложности: Easy
- Последнее обновление: 19 Сен, 2022
Улучшить статью
Сохранить статью
Предпосылки: ключевое слово Yield и итераторы При обсуждении генераторов используются два термина.
Функция-генератора: Функция-генератор определяется как обычная функция, но всякий раз, когда ей нужно сгенерировать значение, она делает это с ключевым словом yield, а не с возвратом. Если тело определения содержит yield, функция автоматически становится функцией-генератором.
Python3
|
Выход
1 2 3
Объект-генератор : Функции-генераторы возвращают объект-генератор. Объекты-генераторы используются либо путем вызова метода next для объекта-генератора, либо с использованием объекта-генератора в цикле for in (как показано в приведенной выше программе).