我正在阅读Python烹饪书,目前正在研究生成器。我觉得很难理解。

由于我有Java背景,那么Java中是否有对等的语言?这本书讲的是“生产者/消费者”,但当我听到这个词时,我想到了线程。

什么是发电机,你为什么要用它?显然,没有引用任何书籍(除非你能直接从一本书中找到一个体面、简单的答案)。如果你慷慨的话,还可以举个例子!


当前回答

它有助于明确区分函数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)时,生成器对象bar为您定义了一个下一个属性。

你可以自己调用它来获取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 3。x语法。__

生成器只是一个函数,它返回一个对象,接下来可以对其调用,这样对于每次调用它都会返回一些值,直到引发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]

Observe that a generator object is generated once, but its code is not run all at once. Only calls to next actually execute (part of) the code. Execution of the code in a generator stops once a yield statement has been reached, upon which it returns a value. The next call to next then causes execution to continue in the state in which the generator was left after the last yield. This is a fundamental difference with regular functions: those always start execution at the "top" and discard their state upon returning a value.

关于这个问题还有很多要说的。例如,可以将数据发送回生成器(参考)。但是,我建议您在了解生成器的基本概念之前不要研究这个问题。

现在你可能会问:为什么要使用生成器?有几个很好的理由:

Certain concepts can be described much more succinctly using generators. Instead of creating a function which returns a list of values, one can write a generator which generates the values on the fly. This means that no list needs to be constructed, meaning that the resulting code is more memory efficient. In this way one can even describe data streams which would simply be too large to fit in memory. Generators allow for a natural way to describe infinite streams. Consider for example the Fibonacci numbers: >>> 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] This code uses itertools.islice to take a finite number of elements from an infinite stream. You are advised to have a good look at the functions in the itertools module, as they are essential tools for writing advanced generators with great ease.


†关于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的回答,我唯一能补充的是建议您看一看David Beazley的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中复制并粘贴整个代码块,并将输出语句更改为我们想要做的任何事情。 如果你复制粘贴代码,你就会被枪毙。你不想中枪,对吧?

为了解决这些问题,并避免被击中,我们可以使用回调函数重写这段代码。每次遇到新的斐波那契数时,我们都会调用回调函数。

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中添加state,允许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()函数时,它将继续执行yield停止的地方。

这比回调函数代码要简洁得多。我们有更干净的代码,更小的代码,更不用说更多的功能代码(Python允许任意大的整数)。

使用列表推导式的经验表明,它们在Python中具有广泛的实用性。然而,许多用例不需要在内存中创建一个完整的列表。相反,它们每次只需要迭代一个元素。

例如,下面的求和代码将在内存中构建一个完整的方块列表,遍历这些值,当引用不再需要时,删除列表:

Sum ([x*x for x in range(10)])

通过使用生成器表达式来节省内存:

求和(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())

more

对于那些具有编程语言和计算背景的人,我喜欢从堆栈框架的角度来描述生成器。

在许多语言中,有一个堆栈在其上面是当前堆栈“帧”。堆栈框架包括分配给函数局部变量的空间,包括传递给该函数的参数。

当你调用一个函数时,当前的执行点(“程序计数器”或类似的东西)被压入堆栈,一个新的堆栈帧被创建。然后执行转移到被调用函数的开始。

对于常规函数,在某个时刻函数返回一个值,堆栈就会“弹出”。函数的堆栈帧将被丢弃,并在之前的位置继续执行。

当函数是生成器时,它可以使用yield语句在不丢弃堆栈帧的情况下返回值。函数中局部变量和程序计数器的值将被保留。这允许生成器在稍后恢复,从yield语句开始继续执行,并且它可以执行更多代码并返回另一个值。

在Python 2.5之前,所有生成器都这样做。Python 2.5还增加了将值传递回生成器的功能。这样,传入的值可以作为yield语句的表达式使用,yield语句从生成器临时返回了控件(和值)。

生成器的关键优势是函数的“状态”被保留,不像常规函数,每次堆栈帧被丢弃,你就会失去所有的“状态”。第二个优点是避免了一些函数调用开销(创建和删除堆栈帧),尽管这通常是一个次要的优点。