GVKun编程网logo

Python yield 小结和实例(python yield yield from)

3

此处将为大家介绍关于Pythonyield小结和实例的详细内容,并且为您解答有关pythonyieldyieldfrom的相关问题,此外,我们还将为您介绍关于Python2.5版yield之学习心得、

此处将为大家介绍关于Python yield 小结和实例的详细内容,并且为您解答有关python yield yield from的相关问题,此外,我们还将为您介绍关于Python 2.5版 yield 之学习心得、python yield、Python yield 使用方法浅析、Python yield 使用浅析的有用信息。

本文目录一览:

Python yield 小结和实例(python yield yield from)

Python yield 小结和实例(python yield yield from)

一个带有 yield 的函数就是一个 generator,它和普通函数不同,生成一个 generator 看起来像函数调用,但不会执行任何函数代码,直到对其调用 next()(在 for 循环中会自动调用 next())才开始执行。虽然执行流程仍按函数的流程执行,但每执行到一个 yield 语句就会中断,并返回一个迭代值,下次执行时从 yield 的下一个语句继续执行。看起来就好像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值。

yield 的好处:把一个函数改写为一个 generator 就获得了迭代能力,比起用类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。

测试代码:

代码如下:
#!/usr/bin/env python
#-*- coding:utf8 -*-

def fab(max):
“””斐波那契數列”””
n, a, b = 0, 0, 1
while n max:
yield b
a, b = b, a + b
n += 1


def perm(items, n=None):
“””全排列”””
if n is None:
n = len(items)
for i in range(len(items)):
v = items[i:i+1]
if n == 1:
yield v
else:
rest = items[:i] + items[i+1:]
for p in perm(rest, n-1):
yield v + p

if __name__ == ‘__main__’:
for n in fab(5):
print n
print “全排列:123”
for n in perm(“123”):
print n


Python 2.5版 yield 之学习心得

Python 2.5版 yield 之学习心得


在 shhgs 发布了关于《 Py 2.5 what’s new 之 yield》之后,原来我不是特别关注 yield 的用法,因为对于2.3中加入的yield相对来说功能简单,它是作为一个 generator 不可缺少的一条语句,只要包含它的函数即是一个 generator 。但在2.3中,generator 不能重入,不能在运行过程中修改,不能引发异常,你要么是顺序调用,要么就创建一个新的 generator。而且 generator 中的 yield 只是一个语句。但到了 2.5 版之后,情况发生了很在的变化。

在 shhgs 的文章中对于 yield 并没有做太多的描述,也因此让我在理解上产生了许多问题,于是我仔细地研究了 What’s new 和 PEP 342 文档,有了一些体会,描述在下面。

这里不说为什么要对 yield 进行修改,只说功能。

1. yield 成为了表达式,它不再是语句,但可以放在单独的行上。原文:

Redefine "yield" to be an expression, rather than a statement. The current yield statement would become a yield expression whose value is thrown away.

可以看到,如果你还是写成语句形式的话,其实还是一个表达式,只是它的值被扔掉了。

那么一个 yield 表达式可以这样写:

x = yield i
y = x + (yield x)

那么这种机制到底是如何工作的呢?在2.3版很容易理解,你完全可以把 yield 语句理解为一个 "return" 语句,只不过 "return" 完后,函数并不结束,而是断续运行,直到再次遇到 yield 语句。那么到了 2.5 版不仅仅是一个 "return" 语句那么简单了,让我们看完下面关于 send() 的说明再描述它吧。

2. 增加了 send(msg) 方法,因此你可以使用它向 generator 发送消息。原文:

Add a new send() method for generator-iterators, which resumes the generator and "sends" a value that becomes the result of the current yield-expression. The send() method returns the next value yielded by the generator, or raises StopIteration if the generator exits without yielding another value.

执行一个 send(msg) 会恢复 generator 的运行,然后发送的值将成为当前 yield 表达式的返回值。然后 send() 会返回下一个被 generator yield 的值,如果没有下一个可以 yield 的值则引发一个异常。

那么可以看过这其实包含了一次运行,从将msg赋给当前被停住的 yield 表达式开始,到下一个 yield 语句结束,然后返回下一个yield语句的参数,然后再挂起,等待下一次的调用。理解起来的确很复杂,不知道你明白了没有。

那么让我们开始想象一下,把 yield 转变为易于理解的东西吧。

我们可以把 yield 想象成下面的伪代码:

x = yield i ==> put(i); x = wait_and_get()

可以看到,可以理解为先是一个 put(i),这个 i 就是 yield 表达式后面的参数,如果 yield 没有参数,则表示 None。它表示将 i 放到一个全局缓冲区中,相当于返回了一个值。

wait_and_get() 可以理解为一个阻塞调用,它等待着外界来唤醒它,并且可以返回一个值。

经过这种转换就容易理解多了。让我们来看一个例子:

>>> def g(): 
    print ’step 1′ 
    x = yield ‘hello’ 
    print ’step 2′, ‘x=’, x 
    y = 5 + (yield x) 
    print ’step 3′, ‘y=’, y 

很简单,每执行一步都显示一个状态,并且打印出相关变量的值,让我们执行一下看一看。

>>> f = g()
>>> f.next()
step 1
‘hello’

看见什么了。当我们执行 next() 时,代码执行到 x = yield ‘hello’ 就停住了,并且返回了 yield 后面的 ‘hello’。如果我们把上面的程序替换成伪代码看一看是什么样子:

def g():
    print ’step 1′
    put(‘hello’)    #x = yield ‘hello’
    x = wait_and get()
    print ’stpe 2′, ’x=’, x
    put(x)
    y = 5 + wait_and_get()
    print ’step 3′, ‘y=’, y

可以从伪代码看出,第一次调用 next() 时,先返回一个 ‘hello’, 然后程序挂起在 x = wait_and_get() 上,与我们执行的结果相同。

让我们继续:

>>> f.send(5)
step 2 x= 5
5

这次我们使用了 send(5) 而不是 next() 了。要注意 next() 在 2.5 中只算是 send(None) 的一种表现方式。正如伪代码演示的,send()一个值,先是激活 wait_and_get() ,并且通过它返回 send(5) 的参数5,于是 x 的值是 5,然后打印 ’step 2′,再返回 x 的值5,然后程序挂起在 y = 5 + wait_and_get() 上,与运行结果一致。

如果我们继续:

>>> f.send(2)
step 3 y= 7

Traceback (most recent call last): 
  File "<pyshell#13>", line 1, in <module> 
    f.send(2) 
StopIteration

可以看到先是激活 wait_and_get(),并且通过它返回 send(2) 的参数 2,因此 y 的值是 7,然后执行下面的打印语句,但因为后面没有下一个 yield 语句了,因此程序无法挂起,于是就抛出异常来。

从上面的伪代码的示例和运行结果的分析,我想你应该对 yield 比较清楚了。还有一些要注意的:

  • next()相当于send(None)
  • yield后面没有参数表示返回为None

在文档中有几句话很重要:

Because generator-iterators begin execution at the top of the generator’s function body, there is no yield expression to receive a value when the generator has just been created. Therefore, calling send() with a non-None argument is prohibited when the generator iterator has just started, and a TypeError is raised if this occurs (presumably due to a logic error of some kind). Thus, before you can communicate with a coroutine you must first call next() or send(None) to advance its execution to the first yield expression.

意思是说,第一次调用时要么使用 next() ,要么使用 send(None) ,不能使用 send() 来发送一个非 None 的值,原因就是第一次没有一个 yield 表达式来接受这个值。如果你转为伪代码就很好理解。以上例来说明,转换后第一句是一个 put() 而不是wait_and_get(),因此第一次执行只能返回,而不能接受数据。如果你真的发送了一个非 None 的值,会引发一个 TypeError 的异常,让我们试一试:

>>> f = g() 
>>> f.send(5)

Traceback (most recent call last): 
  File "<pyshell#15>", line 1, in <module> 
    f.send(5) 
TypeError: can’t send non-None value to a just-started generator

看到了吧,果然出错了。

3. 增加了 throw() 方法,可以用来从 generator 内部来引发异常,从而控制 generator 的执行。试验一下:

>>> f = g() 
>>> f.send(None) 
step 1 
‘hello’ 
>>> f.throw(GeneratorExit)

Traceback (most recent call last): 
  File "<pyshell#17>", line 1, in <module> 
    f.throw(GeneratorExit) 
  File "<pyshell#6>", line 3, in g 
    x = yield ‘hello’ 
GeneratorExit 
>>> f.send(5)

Traceback (most recent call last): 
  File "<pyshell#18>", line 1, in <module> 
    f.send(5) 
StopIteration

可以看出,第一次执行后,我执行了一个f.throw(GeneratorExit),于是这个异常被引发。如果再次执行f.send(5),可以看出 generator 已经被停止了。GeneratorExit 是新增加的一个异常类,关于它的说明:

A new standard exception is defined, GeneratorExit, inheriting from Exception. A generator should handle this by re-raising it (or just not catching it) or by raising StopIteration.

可以看出,增加它的目的就是让 generator 有机会执行一些退出时的清理工作。这一点在 PEP 342 后面的 thumbnail 的例子中用到了。

4. 增加了 close 方法。它用来关闭一个 generator ,它的伪代码如下(从文档中抄来):

def close(self): 
    try: 
        self.throw(GeneratorExit) 
    except (GeneratorExit, StopIteration): 
        pass 
    else: 
        raise RuntimeError("generator ignored GeneratorExit") 
# Other exceptions are not caught

因此可以看出,首先向自身引发一个 GeneratorExit 异常,如果 generator 引发了 GeneratorExit 或 StopIteration 异常,则关闭成功。如果 generator 返回了一个值,则引发 RuntimeError 异常。如果是其它的异常则不作处理,相当于向上层繁殖,由上层代码来处理。关于它的例子在 PEP 342 中的 thumbnail 的例子中也有描述。

还有其它几点变化,不再做更深入的描述。

关于 PEP 342 中的例子也很值得玩味。简单说一些,其实我也些也不是很懂也就是明白个大概其吧。

文档中一共有4个例子,其实是两个例子构成。

1,2两个例子完成了一个 thunmbnail 的处理。第一个例子 consumer 其实是一个 decorator ,它实现了对一个 generator 的封装,主要就是用来调用一次 next() 。为什么,因为这样调一次下一次就可以使用 send() 一个非 None 的值了,这样后面的代码在使用 generator 可以直接使用 send() 非 None 值来处理了。第二个例子完成对一系列的图片的缩略图的处理。这里每个图片的处理做成了一个 generator,对于图片文件的处理又是一个顶层的 generator ,在这个顶层的 generator 来调用每个图片处理的 generator。同时这个例子还实现了当异常退出时的一种保护工作:处理完正在处理的图片,然后退出。

3,4两个例子完成了一个 echo 服务器的演示。3完成了一个调度器,4是在3的基础上将listen处理和socket联通后的handle处理都转为可调度的 generator ,在调度器中进行调度。同时可以看到 socket 使用的是非阻塞的处理。

通过以上的学习,我深深地感受到 yield 的确很精巧,这一点还是在与 shhgs 语音交流之后才有更深的体会,许多东西可以通过 generator 表现得更优美和精巧,是一个非常值得玩味的东西。以至于 shhgs 感觉到在 2.5 中 yield 比 with 的意义要大。希望大家一同体会。

不过说实在的,yield 的东西的确有些难于理解,要仔细体会才行。

 

python yield

python yield

yield

带有 yield 的函数在 Python 中被称之为 generator(生成器),生成器(generator)能够迭代的关键是它有一个next()方法,工作原理就是通过重复调用next()方法,直到捕获一个异常。

生成器与迭代器会在另一篇文章介绍

yield 是一个类似 return的关键字,迭代一次遇到yield时就返回yield后面的值。重点是:下一次迭代时,从上一次迭代遇到的yield后面的代码开始执行。

简要理解:yield就是 return 返回一个值,并且记住这个返回的位置,下次迭代就从这个位置后开始。

首先先举例了解下yield的用法

def myyield():
    print(''start program'')
    while True:
        res = yield ''yes''
        print(''resprint:'',res)


test=myyield()

运行这段代码会怎么样呢?

C:\Users\11573\AppData\Local\Programs\Python\Python36\python.exe D:/pythonwork/socket/test.py

Process finished with exit code 0

奇怪,竟然什么都没有打印,按理说至少会打印一个 start program 才对,但是运行过后什么都没有,这是为什么呢?

next()

程序开始执行以后,因为myyield函数中有yield关键字,所以myyield函数并不会真的执行,而是先得到一个生成器test(相当于一个对象),直到我们调用next方法,myyield函数正式开始执行,先执行myyield函数中的print方法,然后进入while循环

我们修改下代码

def myyield():
    print(''starting program'')
    while True:
        res = yield ''yes''
        print(''resprint:'',res)


test=myyield()
print(next(test))

可以看到结果如下:

C:\Users\11573\AppData\Local\Programs\Python\Python36\python.exe D:/pythonwork/socket/test.py
starting program
yes

有两行输出 但是我们发现并没有执行  

print(''resprint:'',res)

这行代码,这是因为程序遇到yield关键字,然后把yield想像成return,return了一个yes之后,程序停止,并没有执行赋值给res操作,此时next(g)语句执行完成,所以输出的前两行

那么我们再修改下代码:

def myyield():
    print(''starting program'')
    while True:
        res = yield ''yes''
        print(''resprint:'',res)


test=myyield()
print(next(test))
print(''//''*20)
print(next(test))

可以看到输出如下:

C:\Users\11573\AppData\Local\Programs\Python\Python36\python.exe D:/pythonwork/socket/test.py
starting program
yes
////////////////////////////////////////
resprint: None
yes

除了我们用作标记的 ‘////////////////////////////////////////‘ 外 多了两行输入

我们来看下这两行输出分别来自什么语句

首先输出resprint: None 这是因为上次程序中断在yield语句处 这次执行则直接从上次断点继续执行,而上次直接返回值,所以赋值为None 因此第一行输出resprint: None

第二行输出 yes 这是因为程序在while True循环中再次运行到了yield,因此又停止运行

因此通过这个简单的程序我们可以大概知道yield的用法了,带yield的函数是一个生成器,而不是一个函数了,这个生成器有一个函数就是next函数,next就相当于“下一步”生成哪个数,这一次的next开始的地方是接着上一次的next停止的地方继续执行。

send()

yield还有一个send()函数,那么send函数的作用是什么呢?我们可以举例子来看一下send()的作用

我们在上面代码的基础上增加几行代码:

def myyield():
    print(''starting program'')
    while True:
        res = yield ''yes''
        print(''resprint:'',res)


test=myyield()
print(next(test))
print(''//''*20)
print(next(test))
print(''//''*20)
print(test.send(''sending yes''))

看下输出:

C:\Users\11573\AppData\Local\Programs\Python\Python36\python.exe D:/pythonwork/socket/test.py
starting program
yes
////////////////////////////////////////
resprint: None
yes
////////////////////////////////////////
resprint: sending yes
yes

奇怪,之前的resprint都是None,这次却有了赋值,

这是因为,send是发送一个参数给res的,在next函数return的时候,并没有把 ''yes'' 赋值给res,下次执行的时候只好继续执行赋值操作,只好赋值为None了,而如果用send的话,开始执行的时候,先接着上

一次(return yes之后)执行,先把 ''sending yes'' 赋值给了res,然后执行next的作用,遇见下一回的yield,return出结果后结束。send方法中包含next()方法,所以程序会继续向下运行执行print方法,然后再次

进入while循环程序执行再次遇到yield关键字,yield会返回后面的值后,程序再次暂停,直到再次调用next方法或send方法。

 

Python yield 使用方法浅析

Python yield 使用方法浅析

如何生成斐波那契盗

斐波那契(Fibonacci)盗惺且桓龇浅<虻サ牡莨槭校谝桓龊偷诙鍪猓我庖桓鍪伎捎汕傲礁鍪嗉拥玫健S眉扑慊绦蚴涑鲮巢瞧盗械那 N 个数是一个非常简单的问题,许多初学者都可以轻易写出如下函数:

清单 1. 简单输出斐波那契盗星 N 个数

 def fab(max): 
  n,a,b = 0,1 
  while n < max: 
    print b 
    a,b = b,a + b 
    n = n + 1

执行 fab(5),我们可以得到如下输出:

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

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

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

清单 2. 输出斐波那契盗星 N 个数第二版

 def fab(max): 
  n,1 
  L = [] 
  while n < max: 
    L.append(b) 
    a,a + b 
    n = n + 1 
  return L

可以使用如下方式打印出 fab 函数返回的 List:

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

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

清单 3. 通过 iterable 对象来迭代

 for i in range(1000): pass会导致生成一个 1000 个元素的 List,而代码:

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

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

清单 4. 第三个版本

class Fab(object): 

  def __init__(self,max): 
    self.max = max 
    self.n,self.a,self.b = 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() 不断返回数列的下一个数,内存占用始终为常数:

 >>> for n in Fab(5):
 ...     print n
 ...
 1
 1
 2
 3
 5

然而,使用 class 改写的这个版本,代码远远没有第一版的 fab 函数来得简洁。如果我们想要保持第一版 fab 函数的简洁性,同时又要获得 iterable 的效果,yield 就派上用场了:

清单 5. 使用 yield 的第四版

 def fab(max): 
  n,1 
  while n < max: 
    yield b 
    # print b 
    a,a + b 
    n = n + 1 

'''

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

调用第四版的 fab 和第二版的 fab 完全一致:

 >>> 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 的执行流程:

清单 6. 执行流程

 >>> 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 判断:

清单 7. 使用 isgeneratorfunction 判断

 >>> from inspect import isgeneratorfunction 
 >>> isgeneratorfunction(fab) 
 True

要注意区分 fab 和 fab(5),fab 是一个 generator function,而 fab(5) 是调用 fab 返回的一个 generator,好比类的定义和类的实例的区别:

清单 8. 类的定义和类的实例

 >>> 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: 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 的例子来源于文件读取。如果直接对文件对象调用 read() 方法,会导致不可预测的内存占用。好的方法是利用固定长度的缓冲区来不断读取文件内容。通过 yield,我们不再需要编写读文件的迭代类,就可以轻松实现文件读取:

清单 9. 另一个 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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持编程小技巧。

Python yield 使用浅析

Python yield 使用浅析

子邮件发送本页面

Comments

29

您可能听说过,带有 yield 的函数在 Python 中被称之为 generator(生成器),何谓 generator ?

我们先抛开 generator,以一个常见的编程题目来展示 yield 的概念。

如何生成斐波那契數列

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

清单 1. 简单输出斐波那契數列前 N 个数

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,其他函数无法获得该函数生成的数列。

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

清单 2. 输出斐波那契數列前 N 个数第二版

1

2

3

4

5

6

7

8

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

可以使用如下方式打印出 fab 函数返回的 List:

1

2

3

4

5

6

7

8

>>> for n in fab(5):

...     print n

...

1

1

2

3

5

改写后的 fab 函数通过返回 List 能满足复用性的要求,但是更有经验的开发者会指出,该函数在运行中占用的内存会随着参数 max 的增大而增大,如果要控制内存占用,最好不要用 List

来保存中间结果,而是通过 iterable 对象来迭代。例如,在 Python2.x 中,代码:

清单 3. 通过 iterable 对象来迭代

1

for i in range(1000): pass

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

1

for i in xrange(1000): pass

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

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

清单 4. 第三个版本

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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()

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

1

2

3

4

5

6

7

8

>>> for n in Fab(5):

...     print n

...

1

1

2

3

5

然而,使用 class 改写的这个版本,代码远远没有第一版的 fab 函数来得简洁。如果我们想要保持第一版 fab 函数的简洁性,同时又要获得 iterable 的效果,yield 就派上用场了:

清单 5. 使用 yield 的第四版

1

2

3

4

5

6

7

8

9

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 和第一版相比,仅仅把 print b 改为了 yield b,就在保持简洁性的同时获得了 iterable 的效果。

调用第四版的 fab 和第二版的 fab 完全一致:

1

2

3

4

5

6

7

8

>>> 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 的执行流程:

清单 6. 执行流程

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 循环里,无需处理 StopIteration 异常,循环会正常结束。

我们可以得出以下结论:

一个带有 yield 的函数就是一个 generator,它和普通函数不同,生成一个 generator 看起来像函数调用,但不会执行任何函数代码,直到对其调用 next()(在 for 循环中会自动调用 next())才开始执行。虽然执行流程仍按函数的流程执行,但每执行到一个 yield 语句就会中断,并返回一个迭代值,下次执行时从 yield 的下一个语句继续执行。看起来就好像一个函数在正常执行的过程中被 yield 中断了数次,每次中断都会通过 yield 返回当前的迭代值。

yield 的好处是显而易见的,把一个函数改写为一个 generator 就获得了迭代能力,比起用类的实例保存状态来计算下一个 next() 的值,不仅代码简洁,而且执行流程异常清晰。

如何判断一个函数是否是一个特殊的 generator 函数?可以利用 isgeneratorfunction 判断:

清单 7. 使用 isgeneratorfunction 判断

1

2

3

>>> from inspect import isgeneratorfunction

>>> isgeneratorfunction(fab)

True

要注意区分 fab 和 fab(5),fab 是一个 generator function,而 fab(5) 是调用 fab 返回的一个 generator,好比类的定义和类的实例的区别:

清单 8. 类的定义和类的实例

1

2

3

4

5

>>> import types

>>> isinstance(fab, types.GeneratorType)

False

>>> isinstance(fab(5), types.GeneratorType)

True

fab 是无法迭代的,而 fab(5) 是可迭代的:

1

2

3

4

5

>>> from collections import Iterable

>>> isinstance(fab, Iterable)

False

>>> isinstance(fab(5), Iterable)

True

每次调用 fab 函数都会生成一个新的 generator 实例,各实例互不影响:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

>>> 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 的例子来源于文件读取。如果直接对文件对象调用 read() 方法,会导致不可预测的内存占用。好的方法是利用固定长度的缓冲区来不断读取文件内容。通过 yield,我们不再需要编写读文件的迭代类,就可以轻松实现文件读取:

清单 9. 另一个 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

以上仅仅简单介绍了 yield 的基本概念和用法,yield 在 Python 3 中还有更强大的用法,我们会在后续文章中讨论。

注:本文的代码均在 Python 2.7 中调试通过

今天关于Python yield 小结和实例python yield yield from的讲解已经结束,谢谢您的阅读,如果想了解更多关于Python 2.5版 yield 之学习心得、python yield、Python yield 使用方法浅析、Python yield 使用浅析的相关知识,请在本站搜索。

本文标签: