我正在阅读Python烹饪书,目前正在研究生成器。我觉得很难理解。
由于我有Java背景,那么Java中是否有对等的语言?这本书讲的是“生产者/消费者”,但当我听到这个词时,我想到了线程。
什么是发电机,你为什么要用它?显然,没有引用任何书籍(除非你能直接从一本书中找到一个体面、简单的答案)。如果你慷慨的话,还可以举个例子!
我正在阅读Python烹饪书,目前正在研究生成器。我觉得很难理解。
由于我有Java背景,那么Java中是否有对等的语言?这本书讲的是“生产者/消费者”,但当我听到这个词时,我想到了线程。
什么是发电机,你为什么要用它?显然,没有引用任何书籍(除非你能直接从一本书中找到一个体面、简单的答案)。如果你慷慨的话,还可以举个例子!
当前回答
使用列表推导式的经验表明,它们在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
其他回答
生成器实际上是一个函数,它在完成之前返回(数据),但它在该点暂停,您可以在该点恢复该函数。
>>> 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
生成器封装了一个无限循环,但这不是问题,因为每次您都只能得到每个答案。
我相信迭代器和生成器的第一次出现是在Icon编程语言中,大约20年前。
你可能会喜欢Icon的概述,它可以让你在不关注语法的情况下理解它们(因为Icon是一种你可能不知道的语言,Griswold是在向来自其他语言的人解释他的语言的好处)。
在阅读了几段之后,生成器和迭代器的效用可能会变得更加明显。
首先,术语生成器最初在Python中定义不清,导致了很多混乱。你可能指的是迭代器和可迭代对象(参见这里)。然后在Python中还有生成器函数(返回生成器对象)、生成器对象(迭代器)和生成器表达式(求值为生成器对象)。
根据generator的术语表条目,现在的官方术语似乎是generator是“generator function”的缩写。在过去,文档对术语的定义不一致,但幸运的是,这个问题已经得到了解决。
在没有进一步说明的情况下,精确地避免使用术语“生成器”可能仍然是一个好主意。
这篇文章将使用斐波那契数作为工具来解释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