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

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

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

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

Ответ 1

Примечание: этот пост предполагает синтаксис Python 3.x. & dagger;

A генератор - это просто функция, которая возвращает объект, на который вы можете вызвать 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 в генератор (reference). Но я предлагаю вам не заглядывать, пока не поймете базовую концепцию генератора.

Теперь вы можете спросить: зачем использовать генераторы? Есть несколько веских причин:

  • Некоторые понятия могут быть описаны более кратко с использованием генераторов.
  • Вместо создания функции, которая возвращает список значений, можно написать генератор, который генерирует значения "на лету". Это означает, что список не должен быть сконструирован, что означает, что полученный код более эффективен с точки зрения памяти. Таким образом, можно даже описать потоки данных, которые просто были бы слишком большими, чтобы вписаться в память.
  • Генераторы позволяют естественным образом описывать бесконечные потоки. Рассмотрим, например, числа Фибоначчи:

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


  & dagger; О 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

Ответ 2

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

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

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

Ответ 3

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

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

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

Ответ 4

Генераторы могут считаться сокращением для создания итератора. Они ведут себя как 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

Надеемся, что это поможет/это то, что вы ищете.

Update:

Как и многие другие ответы, существуют разные способы создания генератора. Вы можете использовать синтаксис круглых скобок, как в моем примере выше, или вы можете использовать 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
...

Ответ 5

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

Ответ 6

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

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

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

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

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

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

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

Ответ 7

Это помогает сделать четкое различие между функцией foo и генератором foo (n):

def foo(n):
    yield n
    yield n+1

foo - функция. foo (6) - объект-генератор.

Типичный способ использования объекта генератора находится в цикле:

for n in foo(6):
    print(n)

Печать в петлях

# 6
# 7

Подумайте о генераторе как о возобновляемой функции.

yield ведет себя как return в том смысле, что полученные значения получают "возврат" генератором. В отличие от возврата, однако, в следующий раз, когда генератору будет предложено значение, функция-генератор, foo, возобновит то, где она была остановлена - после последнего оператора выписки - и продолжает работать до тех пор, пока он не столкнется с другой операцией 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) вызывает ошибку StopInteration.

Ответ 8

Единственное, что я могу добавить в ответ Stephan202, - это рекомендация, чтобы вы взглянули на презентацию David Beazley PyCon '08 "Генерирующие трюки для системных программистов", которая является лучшим отдельным объяснением того, как и почему генераторы меня Видно где-нибудь. Это то, что взяло меня из "Python выглядит весело" на "Это то, что я искал". Он находится в http://www.dabeaz.com/generators/.

Ответ 9

Это сообщение будет использовать числа Фибоначчи в качестве инструмента для объяснения полезности генераторов 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. Он сохраняет состояние функции и возвращает значение yeilded. В следующий раз, когда вы вызовете следующую функцию() на генераторе, она будет продолжена там, где выход остался.

Это намного более чисто, чем код функции обратного вызова. У нас есть более чистый код, меньший код и не говоря уже о гораздо более функциональном коде (Python допускает сколь угодно большие целые числа).

Источник

Ответ 10

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

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

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

Ответ 11

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

подробнее