python--yield and generator(生成器)简述

1、想象一个场景:

      设想,我想要100个素数,然后对它们累加求和。

通常的想法是,找一个一次性至少能提供100个素数的工具(函数),让它把这100个素数交给我(用return 一次性返回含100个素数的列表)。对于100个素数,这显然是可行的。但是,如果我想要无穷多个素数累加,或者非常非常多素数累加,多到几乎可以消耗掉整个内存空间,这时候找一个工具一次性交付这么多数据就显然非常不合适了。

对于之前的设想,我们还可以有另一种想法,就是找一个能不断生产素数的工具,这个工具并不一次交付100个素数,仅在我跟它索要数据的时候它才给出下一个素数(记住,这个工具是有记忆的,它知道我之前取了哪些数,并给出我现在需要的素数。换句话说,它交付给我一个素数后,它的状态被保留了,它仅仅是被挂起而没有被销毁),有点类似母鸡下蛋,拍一拍便下一个蛋,然后就待产了。试想,如果有这个工具,即使让它提供无穷多素数也完全可行,因为它每次只提供一个素数,完全不存在内存耗尽的问题。那么,这个工具存在吗?当然存在,而且名字就叫生成器(generator)。

2、生成器(generator)原理简介:

在python中有一个关键字yield(yield有生产的意思)。当你在函数中使用了yield,你的函数就发生变化了,它变成了生成器(generator)。当你调用这个函数的时候,它不返回值,而是返回一个生成器对象。一个生成器就这样产生了,一个函数加上一个关键字。虽然简单,但是我们还是迷惑,因为我们还不清楚generator的工作流程。那下面就通过一些简单的例子来跟踪一下它吧。

例1:

python--yield and generator(生成器)简述

我们定义了一个函数(def test()),并使用了关键字yield(yield是生产的意思,其后的'hello'就是它即将生产的值了),于是一个generator就产生了(调用test(),返回了一个generator object test)。我们把这个generator赋给变量h。现在我们用next()启动生成器(注:python2.x可以写作h.next()),产生了一个值'hello'。

例2:接例1,如果我们继续使用next(),再次启动generator会怎样呢?

python--yield and generator(生成器)简述

结果发生了StopIterator(终止迭代器)异常。为什么会这样呢,为什么没有产生'hello',难道再次启动generator时没有从函数(test())的第一行开始执行吗?显然是的,不然就真的输出'hello'了。那第二次启动generator时,函数的入口点在哪里?别急,我们先看下面的例子。

例3:

python--yield and generator(生成器)简述

我在原来test()的基础上又增加了一个yield,第一次启动generator输出'hello',第二次启动generator输出'world'。为什么会这样呢,为什么第一次启动generator没有输出'world'?这就要涉及到generator的工作原理了。

当我们使用next()第一次启动generator的时候,函数的入口点在第一行,也就是说,函数从test()的第一行开始执行。当执行到关键字yield处的时候,返回'hello',同时把控制权交给调用方,自己则把当前状态保留(包括变量值,函数执行状态等),准备被再次调用(还记得‘记忆’吗,是的,generator保留了记忆,而普通函数调用完后所有的数据都要销毁,没有记忆存留)。当第二次启动generator时,函数的入口点变成了>>yield 'world',执行完该语句后,generator继续移交控制权,保存当前状态挂起。

回到例2的问题,为什么会产生StopIterator?根据generator的执行原理,第二次启动generator时,函数入口点在yield 'hello'下一行,而它的一下一行什么都没有,自然会返回异常了。那是不是说我在yield 'hello' 后任加一条语句就不会异常,而且可以执行该语句了呢?在例3中我们加了一条yield语句,结果是很满意的。现在我们再加一条打印语句看看情况吧。

例4:

python--yield and generator(生成器)简述

异常又发生了!第二次启动generator时,print 语句没有执行。为什么会这样呢?原来,generator每次启动执行都要寻找关键字yield,当它找不到下一个yield的时候就会说:“StopIterator”。这也就是为什么例3可以顺利执行,例4却会发生异常。

例5:通过前面的例子,你也许已经大概了解了generator的工作原理,要熟练它还得多练习才行。但是,到这里还只是认识了generator的基础而已。在前几个例子中,yield是作为语句出现的,事实上,它还可以是运算式,这个时候我们需要借助h.send(msg)。先看例子,再详细解释吧。

python--yield and generator(生成器)简述

我在函数中加了一个赋值运算符('=')。第二次启动generator时,我使用了send(msg)来发送消息,msg是消息内容,send()可以像next()一样启动generator,同时它还会把作为其参数的msg发送给yield后面的变量a。记住,如果不给a通过send(msg)赋值,a=None。你是不是还想知道,假设有多个像a一样的变量(var = yield ),send(msg)发送的消息会传给谁呢。我们看下面的例子。

例6:

python--yield and generator(生成器)简述

显然,send('he')把消息送给了b,因为b距离下一个yield最近吗?你可以自己试试。你有没有发现,似乎我们写了这么多函数,但是都没有见到return。在generator中return如何自处呢,或者说return和我们的yield是什么关系呢?

在generator中yield相当于普通函数的return,但是又不完全相同。相似点是,函数在执行过程中碰到return/yield时都会移交控制权,不同点是,return移交后会销毁被调函数的一切状态,yield则将函数暂时挂起。

----------------------------------------------------------------------------------

参考资料:

1、提高你的python:解释'yield'和'generators':http://www.oschina.net/translate/improve-your-python-yield-and-generators-explained

2、python Gossip:yield 产生器:http://openhome.cc/Gossip/Python/YieldGenerator.html

3、小记python yield 与生成器:http://blog.bitfoc.us/?p=502

上一篇:Python pickle模块


下一篇:C#基础知识记录一