Python yield使用浅析(理解生成器作用)
主要参考链接:Python yield使用浅析
在参考链接的资料中,作者以斐波拉契数列为引,从复用性、内存优化、简洁性对代码进行了“升级改造”。首先是基本代码:
版本1
def fab(max): n, a, b = 0, 0, 1 while n < max: print b a, b = b, a + b n = n + 1
这是斐波拉契数列的最直观的实现方法,但函数没有返回值,直接调用后就输出前max个数列,可控性和复用性不高。针对这一点,引入了列表,将数列存于列表中并返回,提高了复用性。代码如下:
版本2
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
该方法返回了一个列表,其中存储着前max个斐波拉契数列。当我们需要依次打印出数列值时,只需要采用如下语句:
for i in fab(5): print i
在这个基础上,又注意到这样一个问题:每当调用fab(N)时,函数就直接生成了含有N个数的list,如果N很大,则会瞬间占用不少内存,而实际往往不需要一次使用这么多数列元素。那么如果优化内存的使用呢?可以使用iterable对象来进行迭代:
版本3
class Fab(object): def __init__(self, max): self.max = max self.n, self.a, self.b = 0, 0, 1 def __iter__(self): return self def next(self): #通过定义Fab类的next方法,使得每次只返回一个迭代后的数列值。占用内存始终为常数。 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()
通过同样的调用方法得到数列:
>>> for n in Fab(5): ... print n ... 1 1 2 3 5
至此,我们得到了一个复用性和内存使用都比较良好的实现方法,但是,付出的代价是可读性和简易性的缺失。
综合以上3个版本的代码实现,我们既想得到iterable的效果,又想让保持代码的简单明了特性,这时就引入了yield关键字的使用,在版本1的基础上,直接将print替换为yield,即可得到版本3的效果,非常方便:
版本4
def fab(max): n, a, b = 0, 0, 1 while n < max: yield b # print b a, b = b, a + b n = n + 1
这个版本的fab函数特性,与版本2一样:
>>> 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 函数就返回一个迭代值,下次迭代时,代码从 yield b 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到 yield。
也可以手动调用 fab(5) 的 next() 方法(因为 fab(5) 是一个 generator 对象,该对象具有 next() 方法),这样我们就可以更清楚地看到 fab 的执行流程:
>>> 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 循环里,无需处理 StopIteration 异常,循环会正常结束。
我们可以得出以下结论:
一个带有 yield 的函数就是一个 generator,它和普通函数不同,生成一个 generator 看起来像函数调用,但不会执行任何函数代码,直到对其调用 next() ( 在 for 循环中会自动调用 next() )才开始执行。虽然执行流程仍按函数的流程执行,但每执行到一个 yield 语句就会中断,并返回一个迭代值,下次执行时从 yield 的下一个语句继续执行。看起来就好像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值。
yield 的好处是显而易见的,把一个函数改写为一个 generator 就获得了迭代能力,比起用类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。
----------------------------------------百无聊赖的分割线----------------------------------------
以下可当做扩展内容,便于更深入的理解生成器概念:
如何判断一个函数是否是一个特殊的 generator 函数?可以利用 isgeneratorfunction 判断:
>>> from inspect import isgeneratorfunction >>> isgeneratorfunction(fab) True
要注意区分 fab 和 fab(5),fab 是一个 generator function,而 fab(5) 是调用 fab 返回的一个 generator,好比类的定义和类的实例的区别:
>>> import types >>> isinstance(fab, types.GeneratorType) False >>> isinstance(fab(5), types.GeneratorType) True
fab 是无法迭代的,而 fab(5) 是可迭代的:
>>> from collections import Iterable >>> isinstance(fab, Iterable) False >>> isinstance(fab(5), Iterable) True
每次调用 fab 函数都会生成一个新的 generator 实例,各实例互不影响:
>>> f1 = fab(3) >>> f2 = fab(5) >>> print 'f1:', f1.next() f1: 1 >>> print 'f2:', f2.next() f2: 1 >>> print 'f1:', f1.next() f1: 1 >>> print 'f2:', f2.next() f2: 1 >>> print 'f1:', f1.next() f1: 2 >>> print 'f2:', f2.next() f2: 2 >>> print 'f2:', f2.next() f2: 3 >>> print 'f2:', f2.next() f2: 5
【return】的作用:
在一个 generator function 中,如果没有 return,则默认执行至函数完毕,如果在执行过程中 return,则直接抛出 StopIteration 终止迭代。
补充例子:
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
以上仅仅简单介绍了 yield 的基本概念和用法,yield 在 Python 3 中还有更强大的用法,作者在后续文章中有所讨论,可循参考链接进行搜寻。
浙公网安备 33010602011771号