Python的yield用法


转载:http://www.ibm.com/developerworks/cn/opensource/os-cn-python-yield/#icomments


  初学Python的开发者经常会发现很多Python函数中用到了yield关键字,然而,带有yield的函数执行流程却和普通函数执行流程不一样,yield到底用来做什么?本文将由浅入深地讲解yield的概念和用法,帮助读者体会Python里yield简单而强大的功能。


  您可能听说过,带有yield的函数在Python中被称为generator(生成器),何谓generator?我们先抛开generator,以一个常见的编程题目来展示yield的概念。


如何生成斐波那契数列

  斐波那契(Fibonacci)数列是一个非常简单的递归数列,除第一个和第二个数外,任意一个数都可由前两个数相加得到。用计算机程序输出斐波那契数列的前N个数是一个非常简单的问题,许多初学者可以轻易写出如下函数:

>>> 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
1
2
3
5

  结果没有问题,但有经验的开发者会指出,直接在fab函数中用print打印数字会导致该函数可复用性差,因为fab函数返回None,其他函数无法获得该函数生成的数列。

  要提高fab函数的可复用性,最好不要直接打印出数列,而是返回一个List。以下是改写后的版本:

>>> 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
... 
>>> for n in fab(5):
...     print n
... 
1
1
2
3
5

  改写后的fab函数通过返回list能满足复用性的要求,但是更有经验的开发者会指出,该函数在运行中占用的内存会随着参数max的增大而增大,如果要控制内存占用,最好不要用list来保存中间结果,而是通过iterable对象来迭代。例如,在Python2.x中,代码:

for i in range(1000):
    pass

  会导致生成一个1000个元素的List,而代码:

for i in xrange(1000):
    pass

  则不会生成一个1000个元素的List,而是在每次迭代中返回下一个数值,内存空间占用很小。因为xrange不返回list,而是返回一个iterable对象。

  利用iterable,我们可以把fab函数改写成一个支持iterable的class,以下是第三个版本的Fab:

>>> 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):
...             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

  Fab类通过next()不断返回数列的下一个数,内存占用始终为常数。然而,使用class改写的这个版本,代码远远没有第一版的fab函数来得简洁。如果我们想要保持第一版fab函数的简洁性,同时又要获得iterable的效果,yield就派上用场了:

>>> def fab(max):
...     n, a, b = 0, 0, 1
...     while n < max:
...             yield b
...             # print b
...             a, b = b, a + b
...             n = n + 1
... 
>>> for n in fab(5):
...     print n
... 
1
1
2
3
5

  这个版本的fab和第一版的fab相比,仅仅把print b改成了yield b,保持简洁性的同时获得了iterable的效果。

  简单地讲,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 'f1', f1.next()
f1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>> print 'f2', f2.next()
f2 3
>>> print 'f2', f2.next()
f2 5
>>> print 'f2', f2.next()
f2
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

return的作用

  在一个generator function中,如果没有return,则默认执行至函数完毕,如果在执行过程中遇到return,则直接抛出StopIteration终止迭代。


另一个例子

  另一个yield的例子来源于文件读取。如果直接对文件对象调用read()方法,会导致不可预测的内存占用。好的方法是利用固定长度的缓冲区来不断读取文件内容。通过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
... 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值