Понимание генераторов в Python
Я сейчас читаю кулинарную книгу по Python и сейчас смотрю на генераторы. Мне трудно заставить мою голову обернуться.
Как я пришел из фона Java, есть ли эквивалент Java? В книге говорилось о "Производителе / Потребителе", однако, когда я слышал, что думаю о потоке.
Что такое генератор и зачем вы его используете? Без цитирования каких-либо книг, очевидно (если вы не можете найти приличный, упрощенный ответ прямо из книги). Возможно, с примерами, если вы чувствуете себя щедрым!
13 ответов
Примечание: этот пост предполагает синтаксис Python 3.x. †
Генератор - это просто функция, которая возвращает объект, для которого вы можете вызвать next
так, что для каждого вызова он возвращает некоторое значение, пока не вызовет StopIteration
исключение, сигнализирующее, что все значения были сгенерированы. Такой объект называется итератором.
Нормальные функции возвращают одно значение, используя return
так же, как в Java. В Python, однако, есть альтернатива, называемая yield
, С помощью yield
где-нибудь в функции делает его генератором. Соблюдайте этот код:
>>> def myGen(n):
... yield n
... yield n + 1
...
>>> g = myGen(6)
>>> next(g)
6
>>> next(g)
7
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Как вы видете, myGen(n)
это функция, которая дает n
а также n + 1
, Каждый звонок next
возвращает одно значение, пока не будут получены все значения. for
петли вызова next
на заднем плане, таким образом:
>>> for n in myGen(6):
... print(n)
...
6
7
Аналогично, существуют выражения-генераторы, которые дают возможность кратко описать некоторые распространенные типы генераторов:
>>> g = (n for n in range(3, 5))
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Обратите внимание, что выражения генератора очень похожи на списочные выражения:
>>> lc = [n for n in range(3, 5)]
>>> lc
[3, 4]
Заметьте, что объект генератора генерируется один раз, но его код запускается не сразу. Только звонки на next
на самом деле выполнить (часть) кода. Выполнение кода в генераторе останавливается один раз yield
оператор был достигнут, после чего он возвращает значение. Следующий звонок next
затем приводит к продолжению выполнения в состоянии, в котором генератор был оставлен после последнего yield
, Это принципиальное отличие от обычных функций: они всегда начинают выполнение сверху и сбрасывают свое состояние при возврате значения.
Есть больше вещей, которые можно сказать по этому вопросу. Например, возможно send
данные обратно в генератор ( ссылка). Но это то, что я предлагаю вам не изучать, пока вы не поймете основную концепцию генератора.
Теперь вы можете спросить: зачем использовать генераторы? Есть несколько веских причин:
- Некоторые концепции могут быть описаны более кратко с использованием генераторов.
- Вместо создания функции, которая возвращает список значений, можно написать генератор, который генерирует значения на лету. Это означает, что не нужно составлять список, что означает, что полученный код более эффективен в памяти. Таким образом, можно даже описать потоки данных, которые просто будут слишком большими, чтобы поместиться в памяти.
Генераторы позволяют естественным образом описывать бесконечные потоки. Рассмотрим, например, числа Фибоначчи:
>>> def fib(): ... a, b = 0, 1 ... while True: ... yield a ... a, b = b, a + b ... >>> import itertools >>> list(itertools.islice(fib(), 10)) [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
Этот код использует
itertools.islice
взять конечное число элементов из бесконечного потока. Рекомендуется внимательно ознакомиться с функциями вitertools
модуль, так как они являются важными инструментами для написания продвинутых генераторов с большой легкостью.
† О Python <=2.6: в приведенных выше примерах next
это функция, которая вызывает метод __next__
на данный объект. В Python <= 2.6 используется немного другая техника, а именно o.next()
вместо next(o)
, Python 2.7 имеет next()
вызов .next
поэтому вам не нужно использовать следующее в 2.7:
>>> g = (n for n in range(3, 5))
>>> g.next()
3
Генератор - это, по сути, функция, которая возвращает (данные) до того, как она завершена, но она останавливается в этой точке, и вы можете возобновить функцию в этой точке.
>>> def myGenerator():
... yield 'These'
... yield 'words'
... yield 'come'
... yield 'one'
... yield 'at'
... yield 'a'
... yield 'time'
>>> myGeneratorInstance = myGenerator()
>>> next(myGeneratorInstance)
These
>>> next(myGeneratorInstance)
words
и так далее. (Или одно) преимущество генераторов заключается в том, что, поскольку они работают с данными по одному фрагменту за раз, вы можете работать с большими объемами данных; со списками, чрезмерные требования к памяти могут стать проблемой. Генераторы, как и списки, являются итеративными, поэтому их можно использовать одинаково:
>>> for word in myGeneratorInstance:
... print word
These
words
come
one
at
a
time
Обратите внимание, что генераторы предоставляют другой способ работы с бесконечностью, например
>>> from time import gmtime, strftime
>>> def myGen():
... while True:
... yield strftime("%a, %d %b %Y %H:%M:%S +0000", gmtime())
>>> myGeneratorInstance = myGen()
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:17:15 +0000
>>> next(myGeneratorInstance)
Thu, 28 Jun 2001 14:18:02 +0000
Генератор инкапсулирует бесконечный цикл, но это не проблема, потому что вы получаете каждый ответ только каждый раз, когда просите его.
Прежде всего, термин " генератор" изначально был несколько нечетким в Python, что приводило к путанице. Вы, вероятно, имеете в виду итераторы и итераторы (см. Здесь). Затем в Python есть также функции генератора (которые возвращают объект генератора), объекты генератора (которые являются итераторами) и выражения генератора (которые оцениваются для объекта генератора).
Согласно записи в глоссарии для генератора, похоже, что официальная терминология теперь такова, что для генератора не хватает "функции генератора". Раньше в документации непоследовательно определялись термины, но, к счастью, это было исправлено.
Это может быть хорошей идеей, чтобы быть точным и избегать термина "генератор" без дальнейшего уточнения.
Генераторы можно рассматривать как сокращение для создания итератора. Они ведут себя как итератор Java. Пример:
>>> g = (x for x in range(10))
>>> g
<generator object <genexpr> at 0x7fac1c1e6aa0>
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> list(g) # force iterating the rest
[3, 4, 5, 6, 7, 8, 9]
>>> g.next() # iterator is at the end; calling next again will throw
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
Надеюсь, что это помогает / это то, что вы ищете.
Обновить:
Как показывают многие другие ответы, существуют разные способы создания генератора. Вы можете использовать синтаксис скобок, как в моем примере выше, или вы можете использовать yield. Еще одна интересная особенность заключается в том, что генераторы могут быть "бесконечными" - итераторы, которые не останавливаются:
>>> def infinite_gen():
... n = 0
... while True:
... yield n
... n = n + 1
...
>>> g = infinite_gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
...
Нет Java-эквивалента.
Вот немного надуманного примера:
#! /usr/bin/python
def mygen(n):
x = 0
while x < n:
x = x + 1
if x % 3 == 0:
yield x
for a in mygen(100):
print a
В генераторе есть цикл, который работает от 0 до n, и если переменная цикла кратна 3, она возвращает переменную.
Во время каждой итерации for
цикл генератор выполнен. Если это первый раз, когда генератор запускается, он запускается с начала, в противном случае он продолжается с предыдущего раза, когда он дал.
Мне нравится описывать генераторы для тех, кто имеет хороший опыт работы с языками программирования и вычислениями, в терминах стековых фреймов.
Во многих языках есть стек, поверх которого находится текущий фрейм стека. Кадр стека включает пространство, выделенное для переменных, локальных для функции, включая аргументы, переданные этой функции.
Когда вы вызываете функцию, текущая точка выполнения ("программный счетчик" или эквивалент) помещается в стек, и создается новый кадр стека. Затем выполнение переходит к началу вызываемой функции.
С обычными функциями в какой-то момент функция возвращает значение, и стек "выталкивается". Кадр стека функции отбрасывается, и выполнение возобновляется в предыдущем месте.
Когда функция является генератором, она может вернуть значение без отбрасывания кадра стека с помощью оператора yield. Значения локальных переменных и счетчик программы внутри функции сохраняются. Это позволяет генератору возобновить работу позднее, продолжив выполнение из оператора yield, и он может выполнить больше кода и вернуть другое значение.
До Python 2.5 это были все генераторы. В Python 2.5 добавлена возможность передачи значений обратно в генератор. При этом переданное значение доступно в виде выражения, полученного из оператора yield, который временно возвратил управление (и значение) из генератора.
Основное преимущество генераторов состоит в том, что "состояние" функции сохраняется, в отличие от обычных функций, где каждый раз, когда кадр стека отбрасывается, вы теряете все это "состояние". Второе преимущество заключается в том, что некоторые из издержек вызова функции (создание и удаление стековых фреймов) исключаются, хотя это, как правило, незначительное преимущество.
Это помогает провести четкое различие между функцией foo и генератором foo(n):
def foo(n):
yield n
yield n+1
фу это функция. foo(6) является объектом-генератором.
Типичный способ использовать объект генератора в цикле:
for n in foo(6):
print(n)
Петля печатает
# 6
# 7
Думайте о генераторе как о возобновляемой функции.
yield
ведет себя как return
в том смысле, что полученные значения "возвращаются" генератором. Однако, в отличие от return, в следующий раз, когда у генератора запрашивается значение, функция генератора foo возобновляет работу с того места, на котором остановилась - после последнего оператора yield - и продолжает работать, пока не достигнет другого оператора yield.
За кулисами, когда вы звоните bar=foo(6)
панель объекта генератора определена для вас, чтобы иметь next
приписывать.
Вы можете вызвать его самостоятельно, чтобы получить значения, полученные из foo:
next(bar) # Works in Python 2.6 or Python 3.x
bar.next() # Works in Python 2.5+, but is deprecated. Use next() if possible.
Когда foo заканчивается (а значений больше нет), вызывается next(bar)
выдает ошибку StopIntate.
Единственное, что я могу добавить к ответу Stephan202 - это рекомендация взглянуть на презентацию Дэвида Бизли PyCon '08 "Уловки генераторов для системных программистов", которая является лучшим объяснением того, как и почему генераторов, которые я видел, я видел. в любом месте. Это то, что привело меня от "Python выглядит довольно забавно" к "Это то, что я искал". Это на http://www.dabeaz.com/generators/.
Этот пост будет использовать числа Фибоначчи как инструмент для объяснения полезности генераторов Python.
Этот пост будет содержать как код C++, так и код Python.
Числа Фибоначчи определяются как последовательность: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, ....
Или вообще:
F0 = 0
F1 = 1
Fn = Fn-1 + Fn-2
Это может быть легко передано в функцию C++:
size_t Fib(size_t n)
{
//Fib(0) = 0
if(n == 0)
return 0;
//Fib(1) = 1
if(n == 1)
return 1;
//Fib(N) = Fib(N-2) + Fib(N-1)
return Fib(n-2) + Fib(n-1);
}
Но если вы хотите напечатать первые шесть чисел Фибоначчи, вы будете пересчитывать многие значения с помощью вышеуказанной функции.
Например: Fib(3) = Fib(2) + Fib(1)
, но Fib(2)
также пересчитывает Fib(1)
, Чем выше значение, которое вы хотите вычислить, тем хуже для вас будет.
Таким образом, можно поддаться искушению переписать вышесказанное, отслеживая состояние в main
,
// Not supported for the first two elements of Fib
size_t GetNextFib(size_t &pp, size_t &p)
{
int result = pp + p;
pp = p;
p = result;
return result;
}
int main(int argc, char *argv[])
{
size_t pp = 0;
size_t p = 1;
std::cout << "0 " << "1 ";
for(size_t i = 0; i <= 4; ++i)
{
size_t fibI = GetNextFib(pp, p);
std::cout << fibI << " ";
}
return 0;
}
Но это очень уродливо, и это усложняет нашу логику в main
, Было бы лучше не беспокоиться о состоянии в нашем main
функция.
Мы могли бы вернуть vector
ценностей и использовать iterator
итерировать этот набор значений, но для большого количества возвращаемых значений требуется много памяти одновременно.
Итак, вернемся к нашему старому подходу, что произойдет, если мы захотим сделать что-то еще, кроме печати чисел? Мы должны были бы скопировать и вставить весь блок кода в main
и измените выходные операторы на все, что мы хотели сделать. А если вы копируете и вставляете код, то вас должны застрелить. Вы не хотите, чтобы вас подстрелили?
Чтобы решить эти проблемы и избежать попадания, мы можем переписать этот блок кода, используя функцию обратного вызова. Каждый раз, когда встречается новый номер Фибоначчи, мы вызываем функцию обратного вызова.
void GetFibNumbers(size_t max, void(*FoundNewFibCallback)(size_t))
{
if(max-- == 0) return;
FoundNewFibCallback(0);
if(max-- == 0) return;
FoundNewFibCallback(1);
size_t pp = 0;
size_t p = 1;
for(;;)
{
if(max-- == 0) return;
int result = pp + p;
pp = p;
p = result;
FoundNewFibCallback(result);
}
}
void foundNewFib(size_t fibI)
{
std::cout << fibI << " ";
}
int main(int argc, char *argv[])
{
GetFibNumbers(6, foundNewFib);
return 0;
}
Это явно улучшение, ваша логика в main
не так загроможден, и вы можете делать все что угодно с числами Фибоначчи, просто определяя новые обратные вызовы.
Но это все еще не идеально. Что, если вы хотите получить только первые два числа Фибоначчи, а затем что-то сделать, потом получить еще немного, а затем сделать что-то еще?
Ну, мы могли бы продолжать, как мы были, и мы могли бы начать добавлять состояние снова в main
, позволяя GetFibNumbers начать с произвольной точки. Но это еще больше раздувает наш код, и он уже выглядит слишком большим для такой простой задачи, как печать чисел Фибоначчи.
Мы могли бы реализовать модель производителя и потребителя через пару потоков. Но это усложняет код еще больше.
Вместо этого давайте поговорим о генераторах.
В Python есть очень хорошая языковая функция, которая решает такие проблемы, как эти, называемые генераторами.
Генератор позволяет вам выполнить функцию, остановиться в произвольной точке, а затем снова продолжить с того места, где вы остановились. Каждый раз возвращая значение.
Рассмотрим следующий код, который использует генератор:
def fib():
pp, p = 0, 1
while 1:
yield pp
pp, p = p, pp+p
g = fib()
for i in range(6):
g.next()
Что дает нам результаты:
0 1 1 2 3 5
yield
оператор используется в сочетании с генераторами Python. Сохраняет состояние функции и возвращает полученное значение. В следующий раз, когда вы вызовете функцию next() в генераторе, она продолжится там, где остановился выход.
Это намного более чисто, чем код функции обратного вызова. У нас более чистый код, меньший код, и не говоря уже о гораздо более функциональном коде (Python допускает произвольно большие целые числа).
Разница в производительности:
macOS Big Sur 11.1
MacBook Pro (13-inch, M1, 2020)
Chip Apple M1
Memory 8gb
ДЕЛО 1
import random
import psutil # pip install psutil
import os
from datetime import datetime
def memory_usage_psutil():
# return the memory usage in MB
process = psutil.Process(os.getpid())
mem = process.memory_info().rss / float(2 ** 20)
return '{:.2f} MB'.format(mem)
names = ['John', 'Milovan', 'Adam', 'Steve', 'Rick', 'Thomas']
majors = ['Math', 'Engineering', 'CompSci', 'Arts', 'Business']
print('Memory (Before): {}'.format(memory_usage_psutil()))
def people_list(num_people):
result = []
for i in range(num_people):
person = {
'id': i,
'name': random.choice(names),
'major': random.choice(majors)
}
result.append(person)
return result
t1 = datetime.now()
people = people_list(1000000)
t2 = datetime.now()
print('Memory (After) : {}'.format(memory_usage_psutil()))
print('Took {} Seconds'.format(t2 - t1))
выход:
Memory (Before): 50.38 MB
Memory (After) : 1140.41 MB
Took 0:00:01.056423 Seconds
- Функция, которая возвращает список
1 million results
. - Внизу я распечатываю использование памяти и общее время.
- Базовое использование памяти было примерно
50.38 megabytes
и это воспоминание после того, как я создал этот список1 million records
так что вы можете видеть здесь, что он подпрыгнул почти1140.41 megabytes
и потребовалось1,1 seconds
.
СЛУЧАЙ 2
import random
import psutil # pip install psutil
import os
from datetime import datetime
def memory_usage_psutil():
# return the memory usage in MB
process = psutil.Process(os.getpid())
mem = process.memory_info().rss / float(2 ** 20)
return '{:.2f} MB'.format(mem)
names = ['John', 'Milovan', 'Adam', 'Steve', 'Rick', 'Thomas']
majors = ['Math', 'Engineering', 'CompSci', 'Arts', 'Business']
print('Memory (Before): {}'.format(memory_usage_psutil()))
def people_generator(num_people):
for i in range(num_people):
person = {
'id': i,
'name': random.choice(names),
'major': random.choice(majors)
}
yield person
t1 = datetime.now()
people = people_generator(1000000)
t2 = datetime.now()
print('Memory (After) : {}'.format(memory_usage_psutil()))
print('Took {} Seconds'.format(t2 - t1))
выход:
Memory (Before): 50.52 MB
Memory (After) : 50.73 MB
Took 0:00:00.000008 Seconds
После того, как я запустил это,
the memory is almost exactly the same
и это потому, что генератор на самом деле ничего не сделал, но он не хранит в памяти тот миллион значений, который ждет меня, чтобы захватить следующее.В основном это
didn't take any time
потому что, как только он доходит до первого оператора yield, он останавливается.Я думаю, что это генератор немного более читабельный, и он также дает вам
big performance boosts not only with execution time but with memory
.Кроме того, вы все еще можете использовать здесь все понимания и это выражение генератора, чтобы ничего не потерять в этой области. Итак, это несколько причин, по которым вы могли бы использовать генераторы, а также некоторые из
the advantages that come along with that
.
Я разместил этот фрагмент кода, который объясняет 3 ключевые концепции генераторов:
def numbers():
for i in range(10):
yield i
gen = numbers() #this line only returns a generator object, it does not run the code defined inside numbers
for i in gen: #we iterate over the generator and the values are printed
print(i)
#the generator is now empty
for i in gen: #so this for block does not print anything
print(i)
Я считаю, что первое появление итераторов и генераторов было на языке программирования Icon около 20 лет назад.
Вам может понравиться обзор Icon, который позволяет вам обдумывать их, не концентрируясь на синтаксисе (поскольку Icon - это язык, который вы, вероятно, не знаете, а Грисволд объяснял преимущества своего языка людям, пришедшим с других языков).
После прочтения всего лишь нескольких абзацев полезность генераторов и итераторов может стать более очевидной.
Опыт работы со списками показал, что они широко используются в Python. Тем не менее, во многих случаях нет необходимости создавать полный список в памяти. Вместо этого им нужно только перебирать элементы по одному.
Например, следующий код суммирования создаст полный список квадратов в памяти, перебирает эти значения и, когда ссылка больше не нужна, удаляет список:
sum([x*x for x in range(10)])
Память сохраняется с помощью выражения генератора вместо:
sum(x*x for x in range(10))
Аналогичные преимущества предоставляются конструкторам для контейнерных объектов:
s = Set(word for line in page for word in line.split())
d = dict( (k, func(k)) for k in keylist)
Выражения генератора особенно полезны с такими функциями, как sum(), min() и max(), которые сводят повторяемый ввод к одному значению:
max(len(line) for line in file if line.strip())