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

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

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

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

11 ответов


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

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

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

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

    >>> 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 в приведенных выше примерах next - это функция, которая вызывает метод __next__ на данный объект. В Python 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, что привело к большой путанице. Вы, вероятно, имеете в виду итераторы и iterables (см. здесь). Тогда в 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 цикл генератор выполняется. Если это первый раз, когда генератор выполняется, он запускается в начале, иначе он продолжается с предыдущего времени, когда он уступил.


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

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

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

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

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

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

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


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


это помогает сделать четкое различие между функцией 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-и продолжает работать, пока не попадет в другой оператор yield.

за кулисами, когда вы называете bar=foo(6) панель объектов генератора определена для вас, чтобы иметь .

вы можете вызвать его самостоятельно, чтобы получить значения, полученные из 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.


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

мы могли бы возвратить 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 Один Один Два Три 5

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

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

источник


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

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

больше