迭代器

要知道生成器是啥,首先得先了解下迭代器是什么,概念的部分还是用我最喜欢的老套路思维导图来表示:


凹脑图在线浏览地址:迭代器

仔细看完这份思维导图后,我们需要区分好两个概念可迭代对象(iterable)迭代器(iterator)

1
2
3
num = [0,1,2,3,4] 
for i in num:
print(i)

这里的列表num符合上面的条件之一:可以for循环,所以列表num可以称之为可迭代对象,那num可以说是迭代器吗?我们可以用isinstance方法来验证下:

1
2
3
In [2]: from collections import Iterator 
In [3]: isinstance(num,Iterator)
Out[3]: False

答案是False,因为列表num并不符合迭代器协议,简单点来讲,列表num里面并没有iter方法next方法。下面我们按照迭代器协议要求自己来构造一个迭代器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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方法来验证:

1
2
In [5]: isinstance(numIter,Iterator) 
Out[5]: False

False!?!这里需要注意的是,numIter只是个类定义,本身是不会迭代的,而numIter(5)这个类的实例才可以进行迭代:

1
2
In [7]: isinstance(numIter(5),Iterator) 
Out[7]: True

生成器

生成器也是一种特殊的迭代器,概念部分继续惯例思维导图贴上:

凹脑图在线浏览地址:生成器

看完了思维导图,我们继续回到上面的那句话生成器也是一种特殊的迭代器,从上面的生成器运行流程中我们不难发现两个身影返回自身对象next方法返回迭代值,这不就是我们上面迭代器讲的迭代器协议(iter方法next方法 )吗?我们还是来用isinstance来验证一下

先用生成器表达式来生成一个表达器:

1
2
3
In [13]: num = (i for i in range(5)) #注意这里使用的是()不是[] 
In [14]: for i in num:
...: print(i)

isinstance验证是否为迭代器:

1
2
In [15]: isinstance(num,Iterator) 
Out[15]: True

答案为true,证明了生成器也是一种迭代器,那为什么要说生成器是一种特殊的迭代器呢?这时我们就得来看另一种生成器的生成方法-生成器函数

1
2
3
4
5
def numGen(n):         #生成器
x = 0
while x < n:
yield x
x += 1

非常简短的几行代码,关键就在于yield这个关键字,一般来说如果我们的函数中出现了yield关键字,调用该函数时就会返回成一个生成器,为了更清楚地理解yield这个关键字的作用,我们还是用代码来说话:

1
2
3
4
5
6
7
8
9
10
11
12
13
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)

首先我们运行第一行代码

1
num = numGen(3) #得到一个生成器对象

得到一个生成器对象,很容易理解的一行代码,但当我们与普通的return方法进行对比时,我们就会发现一个有趣的现象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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函数生成对象时即开始运行内部代码,那生成器函数的代码时什么时候开始执行的呢?别急我们来运行下一行代码:

1
2
3
In [25]: print(num.__next__() 
生成器执行中
0

得出答案,生成器函数的内部代码是在执行next()方法后才开始执行的,新的问题又出现了代码是执行到关键字yield就暂停还是整段代码运行完才暂停,这里我们将上面的例子再次改装(然而我第一次在ipython运行时遇到了一个“bug”,代码如下):

1
2
3
4
5
6
7
8
9
10
11
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,这是生成器的·一个特性:只可以读取一次,所以这里得再重新运行一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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读取文件代码:

1
2
3
4
5
6
7
8
9
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

参考资料:
廖雪峰老师的Python yield 使用浅析
Billy.J.Hee的技术博客
(译)Python关键字yield的解释(stackoverflow)

才学疏浅,欢迎评论指导

评论