Понимание генераторов в Python - PullRequest
189 голосов
/ 18 ноября 2009

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

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

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

Ответы [ 11 ]

355 голосов
/ 18 ноября 2009

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

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


& dagger; О Python <= 2.6: </strong> в приведенных выше примерах next - это функция, которая вызывает метод __next__ для данного объекта. В Python <= 2,6 используется немного другая техника, а именно <code>o.next() вместо next(o). В Python 2.7 есть next() call .next, поэтому вам не нужно использовать следующее в 2.7:

>>> g = (n for n in range(3, 5))
>>> g.next()
3
45 голосов
/ 18 ноября 2009

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

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

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

25 голосов
/ 18 ноября 2009

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

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

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

22 голосов
/ 18 ноября 2009

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

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

8 голосов
/ 19 декабря 2009

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

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

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

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

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

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

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

6 голосов
/ 18 ноября 2009

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

6 голосов
/ 18 ноября 2009

Помогает провести четкое различие между функцией 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 в том смысле, что получаемые значения «возвращаются» генератором. Однако, в отличие от 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.

4 голосов
/ 19 декабря 2009

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

Источник

2 голосов
/ 24 ноября 2017

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

1020 * более *

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...