转自http://blog.itpub.net/29018063/viewspace-2079767
大家在学习python开发时可能经常对迭代器、生成器、yield关键字用法有所疑惑,在这篇文章将从理论+程序调试验证的方式详细讲解这部分知识,话不多说,直接进入主题。
一、迭代器(Iterater):
首先介绍迭代器,迭代器是访问集合元素的一种方式,迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。是不是觉得跟for循环很像?但是迭代器有几个特性需记住:
1、访问者不需要关心迭代器内部结构,只需不断执行next()方法获取下一个内容;
2、不能随机访问集合中的某个值,只能从头到尾顺序的读取;
3、访问到一半时不能回退,不能访问之前的值;
4、适合遍历很大的数据集合,节省内存,提升速度。
根据以上几个特性应该对迭代器有所了解了吧,我这里再举一个例子便于加深理解,大家都玩过linux,在linux中有两个命令,vi和cat,作用都是显示出文件的内容,而当一个文件很大(比如1G)时,使用vi命令打开文件则明显变慢,都有体会吧,会卡很久;而使用cat命令则没有这个问题,无论多大的文件,执行cat命令时都会马上从文件第一行数据开始打印;其实原因也很简单,使用vi时需要将整个文件加载到内存中再显示出来,而使用cat时则从文件第一行记录开始逐行的读取到内存中显示,而已读取过的内容则直接释放掉,这样每次读取到内存中只有一行记录响应速度当然快了。
其实这里的cat正是运用了迭代器的思想,迭代器每次顺序取集合中的一个值到内存,用完即作废,再取下一个值,对应特性1,对于很大的文件遍历则速度很快,对应特性4;则缺点也是明显的,迭代器不能取集合中间某个值,对应特性2;前一个值读取完即回收内存,所以无法重复读取,对应特性3;讲到这里大家应该已经能充分理解迭代器的原理了,下面进行代码演示:
1、创建一个迭代器:
a = iter(["wang","xuqian","xiaozhuzi"]) # 已创建一个迭代器对象,设置好需要迭代的值
2、遍历迭代器数据:
print(a.__next__())
print(a.__next__())
print(a.__next__()) # 有三个值,于是执行三次next()方法
3、结果:
wang
xuqian
xiaozhuzi
就是这么简单,由于迭代器的特性,我们只能顺序依次进行取值,不能像list那样可以取集合中的任意值,在这里三个值都取出后如果再执行a.__next__(),则会报错:“已停止迭代”
Traceback (most recent call last):
print(a.__next__())
StopIteration
二、生成器(Generator)和yield关键字:
生成器定义:当一个函数被调用时,返回一个迭代器,那么这个函数就叫做生成器,如果函数中包括yield语法,则这个函数就是一个生成器
yield:效果就是使函数中断,并保存中断的状态,中断后,代码可以继续往下执行,过一段时间还可以重新调用这个函数,并且可以从上次yield的下面的一句代码开始执行;yield可以返回值,也可以接收send来的参数。
生成器和yield的解释比较抽象难以理解,别急,下面通过调试代码的方式展示他们的用法,以及yield的魅力!~
def cash(account): # 先定义一个函数,接收一个参数
while account > 0: # 当参数>0时
account -= 100 #减去100
yield 100 #中断函数吗,返回100
print("又来取钱啦!") #打印
根据生成器的定义,此时cash函数即为一个生成器,作为一个生成器,当被调用后产生的生成器对象肯定是支持迭代器接口,也就是生成器返回一个迭代器,下面我们进行验证:
a = cash(500) # 调用生成器,生成迭代器
print(a.__next__()) # 运用迭代器的next()方法打印第一个迭代值
执行结果为:100
以上验证了调用生成器cash后的确生成了一个迭代器,关于迭代器应该没有什么疑问了,可以持续next()取值直到 account == 0;而大家现在一定有个疑问,这里面的yield关键字的作用是什么?为什么打印结果没有print("又来取钱啦!")?那么下面我跑一遍程序,通过结果来分析代码执行顺序:
def cash(account):
while account > 0:
account -= 100
yield 100
print("从我开始往下执行!") print("代码已经执行完啦")
a = cash(500)
print("第0次执行我")
print(a.__next__())
print("第一次执行我")
print(a.__next__())
运行结果为:
第0次执行我
100
第一次执行我
从我开始往下执行!
代码已经执行完啦!
100
从运行结果可以推断出,当程序进行到a = cash(500) 时,函数并没有进行调用而是继续往下走打印“第0次执行我”,为什么?其实正是因为在函数定义的时候,检测到函数中写了yield关键字,此时这个函数就变成了一个生成器,于是函数暂停运行;当执行到第一个print(a.__next__()) 时,才开始真正的调用函数了,函数执行到yield 100 时,根据yield的特性,函数中断返回100并打印,程序继续向下执行打印“第一次执行我”;当执行到第二个print(a.__next__()) 时,继续调用函数,根据yield的特性,会接着上一次中断的位置继续执行函数,于是打印“从我开始往下执行!”“代码已经执行完啦”,而根据函数中的while循环,会再次执行循环代码,直到 yield 100,函数再次中断返回100并打印。到此,程序执行完毕!
三、yield单线程异步并发实现
根据以上程序演示,大家应该对yield和生成器的特性理解更加深刻了吧,yield打破了常规的代码执行顺序,甚至能跳出循环,而且下一次调用函数(生成器)时还可以接着上次的代码向下执行,是不是很牛X?那么,yield的特性到底有什么用呢?
举个例子,我去银行取钱,加入取款额度较大,100万~,银行则需要审核、调配资金等手续,假如需要1天的时间,那么这一天我都要等在那里,这显然是不合理的吧。最好的办法是我继续做别的事情,等银行审批结束后再通知我去取钱。对于我们写的代码也是一样,由于我们是单线程串行执行代码,当调用一个函数时,如果这个函数迟迟不返回结果怎么办?按照之前的思路,那么程序只能卡死在那里一直等待,就和去取钱等银行审批一个道理;而yield的特性,打破了这一约束,使得我们在单线程编程的过程中,依然可以实现异步并发!下面再看一段代码:
import time
def consumer(name):
print("%s 准备吃烧烤啦!" %name)
while True:
shaokao = yield
print("烧烤%s被%s吃了!" %(shaokao,name))
def producter(name):
c = consumer("A")
c1 = consumer("B")
c.__next__()
c1.__next__()
print("开始生火烤串啦!")
for i in range(3):
time.sleep(1)
print("烤了两串羊肉")
c.send(i)
c1.send(i)
producter("jiaqi")
根据前面讲的知识,应该可以得出执行结果:
A 准备吃烧烤啦!
B 准备吃烧烤啦!
开始生火烤串啦!
烤了两串羊肉
烧烤0被A吃了!
烧烤0被B吃了!
烤了两串羊肉
烧烤1被A吃了!
烧烤1被B吃了!
烤了两串羊肉
烧烤2被A吃了!
烧烤2被B吃了!
这里就实现了异步并发控制,一个函数和生成器之间调用,通过yield实现;这里面还有个知识点,c.send(i),前面讲过yield不仅能返回值,而且还能接收值,在这里send()大家可以理解为与next()一样,都是触发调用生成器中的代码,但next()可以理解为传一个空值给yield,send()则可传一个实际的值给yield。以上代码中将i值传给了生成器consumer中的Yield