python yield使用浅析

摘自:https://www.ibm.com/developerworks/cn/opensource/os-cn-python-yield/
初学python的人经常会发现python函数中使用了很多yield关键字,然而,带有yield关键字的函数的执行流程和普通函数的执行流程不同,yield带来了什么作用,为什么要设计yield?本文将由浅入深地讲解yield的概念和用法,帮助读者体会yield在python中的简单而强大的功能
您可能听说过,带有yield的函数在python中称为generator(生成器),何谓生成器?先抛开generator,以一个常见的编程题目来展示yield的概念。

如何生成斐波那契数列

版本1(直接输出斐波那契数列)

斐波那契额数列的概念搭建应该比较清晰,相信大家能共很轻易的写出如下的算法来计算斐波那契数列:

1
2
3
4
5
6
def fab(max):
n, a, b = 0, 0, 1
while n < max:
print b
a, b = b, a + b
n = n + 1

执行fab(5),我们会的到如下的结果:

1
2
3
4
5
6
>>> fab(5)
1
1
2
3
5

结果没有问题,但是有经验的开发者会指出,直接在fab函数中 print打印出结果可复用性较差,因为fab返回的结果是None,其他函数无法获取该函数生成的斐波那契数列。所以要提高该函数的可复用性,最好不要直接打印出数列,而是返回一个list

版本二(返回list)

1
2
3
4
5
6
7
8
9
def fab(max):
n, a, b = 0, 0, 1
L = []
while n < max:
L.append(b)
a, b = b, a + b
n = n + 1
return L

使用如下方式打印出斐波那契数列:

1
2
3
4
5
6
7
>>> for n in fab(5):
... print =n
1
1
2
3
5

上述版本获取了可复用性的要求,但是该函数在运行的过程中占用的内存会随着参数max的增大而增大,如果要控制内存占用,最好不要用list来保存中介按结果,而是通过iterable对象来迭代。例如在python2.x中

1
2
3
for i in range(0,100)
for i in xrange(0,100)

前者会生成一个长度为100的list,而后者则不会生成一个100的list,而是在每次迭代中返回下一个数值,内存占用空间很小。因为xrange不返回list,而返回一个iterable的意向,利用iterable我们可以吧fab函数写成一个支持iterable的class

版本三(实现支持iterable的对象)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class fab(object):
def __init__(self, max):
self.max = maxself.n, self.a. self.b = 0, 0, 1
def __iter__(self):
return self
def next(self):
if self.n < self.max:
r = self.b
self.a, self.b = self.b, self.a + self.b
self.n = self.n + 1
return r
raise StopIteration

Fab函数通过next不断返回数列的下一个数,内存占用始终为常数

1
2
3
4
5
6
7
>>>for n in fab(5):
... print n
1
1
2
3
5

上述代码虽然实现了我们版本二的要求,但是代码远远没有第一个版本简洁。如果想要保持第一版的简洁,这个时候就要用上yield

版本四

1
2
3
4
5
6
def fab(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1

第四个版本和第一个版本相比仅仅把print b该成了yield b,就在保持简洁性的同时获得了iterable的效果。调用第四个版本和第二个版本的fab完全一致:

1
2
3
4
5
6
7
>>>for n in fab(5):
... print n
1
1
2
3
5

简单的将,yield的作用就是把一个函数变成了一个generator,带有yield的函数不在是一个普通函数,python解释器会将其视为一个generator,调用fab(5)不会执行fab函数,而是返回一个iterable对象。在for循环执行的时候,每次循环都会执行fab内部的代码,执行到yield b的时候,fab就返回一个迭代之,下次迭代时,代码从yieldb 的下一条语句执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到yield
也可以调用fab(5)的next()方法进行回去每次计算的值。

yield函数的执行流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> f = fab(5)
>>> f.next()
1
>>> f.next()
1
>>> f.next()
2
>>> f.next()
3
>>> f.next()
5
>>> f.next()
Traceback (most recent call last):
File "<stdin>". line 1, in <module>
StopIteration

当函数执行结束的时候,generator自动自动抛出StopIteration的异常,表示迭代的结束,而在for循环中,我们不需要手动的进行处理异常,循环会自动的正常结束。

一个带有yield的函数就是一盒generator,它和普通的函数不同,声称一个generator看起来想函数调用,但是部执行任何函数代码,直到对其调用next()(注意在for循环中会自动调用next)才开始执行。虽然执行流程和普通函数一样,但是每执行到一个yield语句,就会中断,并返回一个迭代值,下次执行的时候从yield的下一个语句开始执行。看起来像是一个函数在正常执行的过程中被yield中断了数次,每次中断都会通过yield返回当前迭代器的值。
yield的好处显而易见,把一个函数该写成generator就获得了迭代能力,比起在类的实例中保存状态计算下一个next的值,更加使代码清洁,而且执行流程非常清晰

判断是否为generator

方法是使用isgeneratorfunction来进行判断

1
2
from inspect import isgeneratorfunction
isgeneratorfunction(fab)

注意fab不可迭代,而fab(5)可迭代

return的作用

在一个generator function中,若函数中没有return语句,默认为函数执行到函数结尾,而如果中间遇到return语句,则直接判处StopIteration异常,结束迭代。
使用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

上述当文件读取完毕的时候奖直接返回StopIteration异常,结束迭代。

Comment and share

  • page 1 of 1

魏传柳(2824759538@qq.com)

author.bio


Tencent


ShenZhen,China