Понимание генераторов в Python

218

Я сейчас читаю кулинарную книгу по Python и сейчас смотрю на генераторы. Мне трудно заставить мою голову обернуться.

Как я пришел из Java-фона, есть ли Java-эквивалент? В книге говорилось о «Производителе / ​​Потребителе», однако, когда я слышу, что думаю о потоке.

Что такое генератор и зачем вы его используете? Без цитирования каких-либо книг, очевидно (если только вы не можете найти приличный, упрощенный ответ прямо из книги). Возможно, с примерами, если вы чувствуете себя щедрым!

Федерер
источник

Ответы:

402

Примечание: этот пост предполагает синтаксис 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
Stephan202
источник
9
Вы упоминаете, что это возможно для sendданных к генератору. Как только вы это сделаете, у вас будет «сопрограмма». Реализовать такие шаблоны, как упомянутый Consumer / Producer, с сопрограммами очень просто, потому что им не нужны Locks и, следовательно, они не могут зайти в тупик. Трудно описать сопрограммы без разбивки потоков, поэтому я просто скажу, что сопрограммы - очень элегантная альтернатива многопоточности.
Йохен Ритцель
Являются ли генераторы Python машинами Тьюринга с точки зрения их функционирования?
Огненный Феникс
48

Генератор - это, по сути, функция, которая возвращает (данные) до ее завершения, но в этот момент она останавливается, и вы можете возобновить функцию в этой точке.

>>> 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   

Генератор инкапсулирует бесконечный цикл, но это не проблема, потому что вы получаете каждый ответ каждый раз, когда просите его.

Калеб Хаттинг
источник
30

Прежде всего, термин « генератор» изначально был несколько нечетким в Python, что приводило к путанице. Вы, вероятно, имеете в виду итераторы и итерации (см. Здесь ). Затем в Python есть также функции генератора (которые возвращают объект генератора), объекты генератора (которые являются итераторами) и выражения генератора (которые оцениваются для объекта генератора).

Согласно записи глоссария для генератора, кажется, что официальная терминология теперь такова, что генератор сокращён от «функции генератора». Раньше в документации непоследовательно определялись термины, но, к счастью, это было исправлено.

Это может быть хорошей идеей, чтобы быть точным и избегать термина «генератор» без дальнейшего уточнения.

nikow
источник
2
Хм, я думаю, что вы правы, по крайней мере, согласно тесту из нескольких строк в Python 2.6. Выражение генератора возвращает итератор (он же «объект генератора»), а не генератор.
Крейг Маккуин
22

Генераторы можно рассматривать как сокращение для создания итератора. Они ведут себя как итератор 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
...
overthink
источник
1
Теперь у Java есть Streams, которые гораздо больше похожи на генераторы, за исключением того, что вы, очевидно, не можете просто получить следующий элемент без удивительного количества хлопот.
Фонд Моники Иск
12

Нет эквивалента 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цикла выполняется генератор. Если это первый раз, когда генератор запускается, он запускается с начала, в противном случае он продолжается с предыдущего раза, когда он дал.

Wernsey
источник
2
Последний абзац очень важен: состояние функции генератора «замораживается» каждый раз, когда она возвращает sth, и продолжается в том же состоянии, когда она вызывается в следующий раз.
Йоханнес Чарра
В Java нет синтаксического эквивалента «выражению генератора», но генераторы - если они у вас есть - по сути просто итератор (те же основные характеристики, что и итератор Java).
думаю,
@ overthink: у генераторов могут быть и другие побочные эффекты, которых нет у итераторов Java. Если бы я поместил print "hello"после x=x+1в моем примере, «hello» было бы напечатано 100 раз, в то время как тело цикла for было бы выполнено только 33 раза.
Вернси
@iWerner: Я уверен, что такой же эффект мог бы быть в Java. Реализация next () в эквивалентном Java-итераторе все равно должна будет искать от 0 до 99 (используя ваш пример mygen (100)), поэтому вы можете каждый раз, когда захотите, System.out.println (). Вы бы вернулись только 33 раза из следующего (), хотя. В Java не хватает очень удобного синтаксиса yield, который значительно легче читать (и писать).
думаю,
Я любил читать и запоминать эту единственную строку def: если это первый раз, когда генератор запускается, он запускается в начале, в противном случае он продолжается с предыдущего раза, когда он выдал.
Икра.
8

Мне нравится описывать генераторы для тех, кто имеет хороший опыт работы с языками программирования и вычислениями, в терминах стековых фреймов.

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

Когда вы вызываете функцию, текущая точка выполнения («программный счетчик» или эквивалент) помещается в стек, и создается новый кадр стека. Затем выполнение переходит к началу вызываемой функции.

С обычными функциями в какой-то момент функция возвращает значение, и стек «выталкивается». Кадр стека функции отбрасывается, и выполнение возобновляется в предыдущем месте.

Когда функция является генератором, она может вернуть значение без отбрасывания кадра стека, используя оператор yield. Значения локальных переменных и счетчик программы внутри функции сохраняются. Это позволяет генератору возобновить работу позднее, продолжив выполнение из оператора yield, и он может выполнить больше кода и вернуть другое значение.

До Python 2.5 это были все генераторы. В Python 2.5 добавлена ​​возможность передачи значений обратно в генератор. При этом переданное значение доступно в виде выражения, полученного из оператора yield, который временно вернул управление (и значение) из генератора.

Основное преимущество генераторов состоит в том, что «состояние» функции сохраняется, в отличие от обычных функций, где каждый раз, когда кадр стека отбрасывается, вы теряете все это «состояние». Второе преимущество заключается в том, что некоторые из издержек вызова функции (создание и удаление стековых фреймов) исключаются, хотя обычно это незначительное преимущество.

Питер Хансен
источник
7

Это помогает провести четкое различие между функцией 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)вызывает ошибку StopInstruction.

unutbu
источник
6

Единственное, что я могу добавить к ответу Stephan202 - это рекомендация взглянуть на презентацию Дэвида Бизли PyCon '08 «Уловки генераторов для системных программистов», которая является лучшим объяснением того, как и почему генераторов, которые я видел, я видел. везде. Это то, что привело меня от «Python выглядит довольно забавно» к «Это то, что я искал». Это на http://www.dabeaz.com/generators/ .

Роберт Россни
источник
5

Этот пост будет использовать числа Фибоначчи как инструмент для объяснения полезности генераторов 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значения a и использовать 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 допускает произвольно большие целые числа).

Источник

Брайан Р. Бонди
источник
3

Я считаю, что первое появление итераторов и генераторов было на языке программирования Icon около 20 лет назад.

Вам может понравиться обзор Icon , который позволяет вам обдумывать их, не концентрируясь на синтаксисе (поскольку Icon - это язык, который вы, вероятно, не знаете, а Грисволд объяснял преимущества своего языка людям, пришедшим с других языков).

После прочтения всего лишь нескольких абзацев полезность генераторов и итераторов может стать более очевидной.

Nosredna
источник
2

Опыт работы со списками показал, что они широко используются в 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())

Больше

Сакиб Муйтаба
источник
2

Я поднял этот фрагмент кода, который объясняет 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)
Стефан Янку
источник