了解Python中的生成器
现在阅读Python食谱,目前正在查看发电机。 我很难find我的头。
因为我来自Java背景,有没有Java的等价物? 这本书讲的是“生产者/消费者”,但是当我听说我想到穿线。
任何人都可以解释一个发电机是什么,为什么你会使用它? 没有引用任何书籍,显然(除非你可以从一本书直接find一个体面的,简单的答案)。 也许举个例子,如果你感到慷慨!
注意:这篇文章假定Python 3.x语法。 †
生成器只是一个函数,它返回一个可以在其中调用的对象,这样每个调用都会返回一个值,直到引发StopIteration
exception,表示已经生成了所有的值。 这样的对象被称为迭代器 。
正常的函数使用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
同样,还有一些生成器expression式 ,它们提供了简洁地描述某些常见types的生成器的方法:
>>> 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
请注意,生成器expression式很像列表parsing :
>>> lc = [n for n in range(3, 5)] >>> lc [3, 4]
观察一个生成器对象是否会生成一次 ,但是它的代码不会一次全部运行。 只有调用next
实际执行(部分)的代码。 生成器中代码的执行在达到yield
语句后停止,并返回一个值。 接下来的下一个调用将导致执行在最后一个yield
之后发生器被遗留的状态next
继续。 这与常规函数有一个根本的区别:那些函数总是从“top”开始执行,并在返回值时抛弃它们的状态。
关于这个问题还有更多要说的。 例如,可以send
数据send
回发生器( 参考 )。 但是,这是我build议你直到你了解发电机的基本概念之前不要看。
现在你可能会问:为什么使用发电机? 有几个很好的理由:
- 某些概念可以用发电机更简洁地描述。
- 我们可以不用创build一个返回值列表的函数,而是可以编写一个生成器来生成值。 这意味着不需要构build任何列表,这意味着生成的代码更具有内存效率。 通过这种方式,甚至可以描述数据stream,这些数据stream可能太大而不适合存储器。
-
发电机允许自然的方式来描述无限的stream。 考虑例如斐波纳契数字 :
>>> 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
从无限stream中获取有限数量的元素。 build议您仔细阅读itertools
模块中的函数,因为它们是编写高级生成器的基本工具,非常容易。
† 关于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
生成器实际上是一个在完成之前返回(数据)的函数,但是在那一刻暂停,并且可以在该点恢复该函数。
>>> 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
生成器封装了一个无限循环,但是这不是一个问题,因为每次你只需要得到每个答案。
生成器可以被认为是创build迭代器的简写。 它们的行为就像一个Java Iterator。 例:
>>> 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
希望这有助于/正在寻找什么。
更新:
正如许多其他答案显示,有不同的方法来创build一个生成器。 您可以使用上面我的示例中的括号语法,也可以使用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 ...
首先,术语生成器最初在Python中有些不明确,导致很多混淆。 你可能意思是迭代器和迭代 器 (见这里 )。 然后在Python中,还有生成器函数 (返回一个生成器对象), 生成器对象 (它们是迭代器)和生成器expression式 (它们被评估为一个生成器对象)。
根据http://docs.python.org/glossary.html#term-generator ,似乎官方术语现在是发电机是“发电机function”的简称。 在过去,文档定义的条款不一致,但幸运的是这个问题已经得到解决。
精确的说法可能仍然是一个好主意,避免使用术语“发生器”而不做进一步的说明。
没有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的循环,如果循环variables是3的倍数,它将产生variables。
在for循环的每次迭代过程中,都会执行生成器。 如果这是发电机第一次执行,它从一开始就开始,否则它继续从它以前的时间
我唯一可以添加到Stephan202的答案是build议您看看David Beazley的PyCon '08演示文稿“系统程序员的发电机技巧”,这是对我所见过的发电机的原理和原因的最好解释任何地方。 这是把我从“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
将发电机想象成可恢复的function。
yield
行为就像return
一样,所产生的值被发生器“返回”。 然而,与return不同的是,当生成器下一次被询问一个值时,生成器的函数foo会从最后一个yield语句之后的地方恢复,并继续运行直到遇到另一个yield语句。
在幕后,当你调用bar=foo(6)
,生成器对象栏被定义为具有next
属性。
你可以自己调用它来检索从foo产生的值:
next(bar) # works in python2.6 or python3.x bar.next() # works in python2.5+, but is deprecated. Use next() if possible.
当foo结束时(并且没有更多的取值),调用next(bar)
会引发StopInteration错误。
我喜欢用堆栈框架来描述那些具有编程语言和计算背景的人。
在很多语言中,有一个堆栈,其上是当前堆栈“框架”。 堆栈帧包含分配给函数本地variables的空间,包括传递给该函数的参数。
当你调用一个函数时,当前的执行点(“程序计数器”或等价物)被压入堆栈,并创build一个新的堆栈帧。 然后执行转移到被调用的函数的开头。
使用正则函数,在某个时候函数返回一个值,并且堆栈被“popup”。 函数的堆栈帧被丢弃,执行在前一个位置恢复。
当一个函数是一个生成器时,它可以使用yield语句返回一个没有丢弃堆栈帧的值。 保存函数中局部variables和程序计数器的值。 这允许生成器稍后恢复,并从yield语句继续执行,并且可以执行更多的代码并返回另一个值。
在Python 2.5之前,这是所有的发生器。 Python 2.5添加了将值传回给生成器的function。 在这样做的时候,传入的值可以作为一个从yield语句中得到的expression式,该语句临时返回了来自generator的控制(和一个值)。
生成器的关键优势在于,函数的“状态”被保留,与每次丢弃堆栈帧的常规函数不同,您将失去所有的“状态”。 第二个优点是避免了一些函数调用开销(创build和删除堆栈帧),尽pipe这通常是次要的优点。
我相信大约20年前,迭代器和生成器的第一次出现是用Icon编程语言编写的。
你可能会喜欢Icon概述 ,它可以让你围绕在他们身边,而不用集中在语法上(因为Icon是一种你可能不知道的语言,而Griswold正在向来自其他语言的人们解释他的语言的好处)。
在阅读了几段后,发生器和迭代器的效用可能会变得更加明显。
这篇文章将使用斐波那契数字作为工具来build立解释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); }
但是如果你想打印前6个斐波纳契数字,你将重新计算上述函数的很多值。
例如: Fib(3) = Fib(2) + Fib(1)
,但Fib(2)
也重新计算Fib(1)
。 你想计算的价值越高,你的情况就越糟糕。
所以有人可能会试图通过跟踪main
状态来重写上述内容。
//Not supported for the first 2 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
function的状态。
我们可以返回一个vector
量值,并使用一个iterator
迭代这组值,但是这需要大量的内存来处理大量的返回值。
回到我们以前的做法,如果除了打印数字之外还想做其他事情,会发生什么? 我们必须复制并粘贴main
的所有代码块,并将输出语句更改为我们想要执行的任何操作。 如果你复制并粘贴代码,那么你应该被枪杀。 你不想被枪杀吗?
为了解决这些问题,为了避免被枪杀,我们可以使用callback函数来重写这段代码。 每次遇到一个新的斐波那契数字,我们都会调用callback函数。
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
逻辑并不杂乱,你可以做任何你想要的与斐波那契数字,只需定义新的callback。
但是这还不够完美。 如果你只想得到前两个斐波纳契数,然后做点什么,再多做点什么,然后做点别的。
那么我们可以继续像我们一样,我们可以开始在main
添加状态,允许GetFibNumbers从任意点开始。 但是这会进一步膨胀我们的代码,而且对于像打印斐波那契数字这样的简单任务,它已经看起来太大了。
我们可以通过几个线程来实现一个生产者和消费者模型。 但是这使代码更复杂。
相反,我们来谈谈发电机。
Python有一个很好的语言function,可以解决这些被称为生成器的问题。
一个生成器允许你执行一个函数,在任意一点停止,然后再继续你离开的地方。 每次返回一个值。
考虑以下使用生成器的代码:
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
五
yield
语句与Python生成器结合使用。 它保存函数的状态并返回yeted值。 下一次你调用发生器的next()函数时,它将继续在产量下降的地方。
这比callback函数代码干净得多。 我们有更干净的代码,更小的代码,更不用说更多的function性代码(Python允许任意大的整数)。
资源
- 出于好奇:如何生成序列号? 提示,algorithm?
- 如何在表单寄存器(laravel generator infyom)中添加一些文本字段?
- Python:使用recursionalgorithm作为生成器
- Koa / Co / Bluebird或Q / Generators / Promises / Thunk相互影响? (Node.js)
- 接受Markdown文档的静态网站生成器的build议?
- 如何outlookPython生成器中的一个元素?
- 为什么列表推导写入循环variables,但生成器不?
- 有没有一个机制在ES6(ECMAScript 6)没有可变variables循环x次?
- 在列表中确定连续重复的最奇怪的方法是什么?