迭代器
要知道生成器是啥,首先得先了解下迭代器是什么,概念的部分还是用我最喜欢的老套路思维导图来表示:
仔细看完这份思维导图后,我们需要区分好两个概念可迭代对象(iterable)与迭代器(iterator)
num = [0,1,2,3,4] for i in num: print(i)
这里的列表num符合上面的条件之一:可以for循环,所以列表num可以称之为可迭代对象,那num可以说是迭代器吗?我们可以用isinstance方法来验证下:
In [2]: from collections import Iterator In [3]: isinstance(num,Iterator) Out[3]: False
答案是False,因为列表num并不符合迭代器协议,简单点来讲,列表num里面并没有__iter__方法和__next__方法。下面我们按照迭代器协议要求自己来构造一个迭代器:
class numIter: #迭代器 def __init__(self,n): self.n = n def __iter__(self): self.x = -1 return self def __next__(self): #Python 3.x版本 Python 2.x版本是next() self.x += 1 if self.x < self.n: return self.x else: raise StopIteration for i in numIter(5): print(i)
numIter里面包含了_iter_方法和_next_方法,符合了迭代器协议,numIter是不是迭代器呢?下面我们继续使用isinstance方法来验证:
In [5]: isinstance(numIter,Iterator) Out[5]: False
False!?!这里需要注意的是,numIter只是个类定义,本身是不会迭代的,而numIter(5)这个类的实例才可以进行迭代:
In [7]: isinstance(numIter(5),Iterator) Out[7]: True
生成器
生成器也是一种特殊的迭代器,概念部分继续惯例思维导图贴上:
看完了思维导图,我们继续回到上面的那句话生成器也是一种特殊的迭代器,从上面的生成器运行流程中我们不难发现两个身影返回自身对象和next方法返回迭代值,这不就是我们上面迭代器讲的迭代器协议(__iter__方法和__next__方法)吗?我们还是来用isinstance来验证一下
先用生成器表达式来生成一个表达器:
In [13]: num = (i for i in range(5)) #注意这里使用的是()不是[] In [14]: for i in num: ...: print(i)
isinstance验证是否为迭代器:
In [15]: isinstance(num,Iterator) Out[15]: True
答案为true,证明了生成器也是一种迭代器,那为什么要说生成器是一种特殊的迭代器呢?这时我们就得来看另一种生成器的生成方法-生成器函数:
def numGen(n): #生成器 x = 0 while x < n: yield x x += 1
非常简短的几行代码,关键就在于yield这个关键字,一般来说如果我们的函数中出现了yield关键字,调用该函数时就会返回成一个生成器,为了更清楚地理解yield这个关键字的作用,我们还是用代码来说话:
In [19]: num = numGen(3) #得到一个生成器对象 In [20]: print(num.__next__()) #执行next方法 0 In [21]: print(num.__next__()) 1 In [22]: print(num.__next__()) 2 In [23]: print(num.__next__()) --------------------------------------------------------------------------- StopIteration Traceback (most recent call last)
首先我们运行第一行代码
num = numGen(3) #得到一个生成器对象
得到一个生成器对象,很容易理解的一行代码,但当我们与普通的return方法进行对比时,我们就会发现一个有趣的现象:
def numGen(n): #生成器 x = 0 print("生成器执行中") while x < n: yield x x += 1 def numGen1(n): x = 0 print("普通方法执行中") while x < n: x += 1 return x num = numGen(3) num1 = numGen1(3) 输出:普通方法执行中
从这个例子我们就可以看出,当我们生成一个生成器对象时,生成器函数内部的代码并不会马上执行,而普通return函数生成对象时即开始运行内部代码,那生成器函数的代码时什么时候开始执行的呢?别急我们来运行下一行代码:
In [25]: print(num.__next__() 生成器执行中 0
得出答案,生成器函数的内部代码是在执行next()方法后才开始执行的,新的问题又出现了代码是执行到关键字yield就暂停还是整段代码运行完才暂停,这里我们将上面的例子再次改装(然而我第一次在ipython运行时遇到了一个“bug”,代码如下):
In [26]: def numGen(n): #生成器 ...: x = 0 ...: print("生成器执行yield前") ...: while x < n: ...: yield x ...: print("生成器执行yield后") ...: x += 1 ...: In [27]: print(num.__next__()) 1
这其实不是bug,这是生成器的·一个特性:只可以读取一次,所以这里得再重新运行一次:
In [1]: def numGen(n): #生成器 ...: x = 0 ...: print("生成器执行yield前") ...: while x < n: ...: yield x ...: print("生成器执行yield后") ...: x += 1 ...: In [2]: num = numGen(3) In [3]: print(num.__next__()) 生成器执行yield前 0 In [4]: print(num.__next__()) 生成器执行yield后 1 In [5]: print(num.__next__()) 生成器执行yield后 2 In [6]: print(num.__next__()) 生成器执行yield后 --------------------------------------------------------------------------- StopIteration Traceback (most recent call last)
有了这个例子,我们就能很好地理解关键字yield的作用了,当代码运行到关键字yield时,执行中断并返回当前的迭代值,除此之外当前的上下文环境也会被记录下来,简单点讲就是执行中断的位置和数据都被保存起来。再次使用 next() 的时候,从原来中断的地方继续执行,直至遇到 yield,如果没有 yield,则抛出StopIteration 异常。
了解了生成器的运行机制,最后我们再来了解下生成器其余的三种方法
send()方法
In [3]: def numGen(n): #生成器 ...: x = 0 ...: while x < n: ...: y = yield x ...: print(y) ...: x += 1 ...: num = numGen(3) ...: print(num.__next__()) ...: print(num.send(999)) ...: print(num.__next__()) ...: print(num.__next__()) ...: 0 999 1 None 2 None --------------------------------------------------------------------------- StopIteration Traceback (most recent call last)
下面来说下运行流程:
首先调用next()方法,让生成器内部代码执行到关键字yield处,返回0;
接着调用send(999)方法,将值999传到代码执行中断的地方,也就是关键字yield处,将999赋值给y,输出y,执行x+=1,执行到关键字yield处,返回1;
继续调用next()方法,无值赋给y,y=None,输出y,执行x+=1,执行到关键字yield处,返回2;
继续调用next()方法,无值赋给y,y=None,输出y,执行x+=1,x=n跳出while循环,找不到关键字yield,抛出StopIteration 异常;
throw()方法
In [4]: def numGen(n): #生成器 ...: try: ...: x = 0 ...: while x < n: ...: yield x ...: x += 1 ...: except ValueError: ...: yield 'Error' ...: finally: ...: print('Finally') ...: ...: num = numGen(3) ...: print(num.__next__()) ...: print(num.throw(ValueError)) ...: print(num.__next__()) ...: 0 Error Finally --------------------------------------------------------------------------- StopIteration Traceback (most recent call last)
可以看出当我们向生成器抛去ValueError错误时,整个生成器就执行finally,最后抛出StopIteration 异常;
close()方法
In [5]: def numGen(n): #生成器 ...: x = 0 ...: while x < n: ...: yield x ...: x += 1 ...: ...: num = numGen(3) ...: print(num.__next__()) ...: num.close() ...: print(num.__next__()) ...: 0 --------------------------------------------------------------------------- StopIteration Traceback (most recent call last)
当我们运行close()方法时,整个生成器就终止了,再执行next()方法,就抛出StopIteration 异常;
最后,学了这么多,生成器到底有什么过人之处:
1)由于生成器这种“走停走停”策略,使得生成器可以逐步生成序列,不用像list一样初始化时就要开辟所有的空间,所以当你一次只需对一个数进行处理时,使用生成器是一个不错的选择。
2)运用好生成器的四种方法next()throw()send()close()还有生成器的关键字yield的特性,是可以实现伪并发操作的,Python虽然支持多线程,可由于GIL(全局解释锁)的存在,使得同一时刻只能有一条线程运行,并没有办法并行操作,所以Python的多线程实际上就是鸡肋。
3)我们在读取文件时,如果直接对文件对象调用 read() 方法,会导致不可预测的内存占用。好的方法是利用固定长度的缓冲区来不断读取文件内容。通过 yield,我们不再需要编写读文件的迭代类,就可以轻松实现文件读取:
下面贴上廖雪峰老师的yield读取文件代码:
def read_file(fpath): BLOCK_SIZE = 1024 with open(fpath, 'rb') as f: while True: block = f.read(BLOCK_SIZE) if block: yield block else: return
才学疏浅,欢迎评论指导
参考资料: