Python中的生成器和yield详细介绍
列表推导与生成器表达式
当我们创建了一个列表的时候,就创建了一个可以迭代的对象:
>>>squares=[n*nforninrange(3)] >>>foriinsquares: printi 0 1 4
这种创建列表的操作很常见,称为列表推导。但是像列表这样的迭代器,比如str、file等,虽然用起来很方便,但有一点,它们是储存在内存中的,如果值很大,会很麻烦。
而生成器表达式不同,它执行的计算与列表包含相同,但会迭代的生成结果。它的语法与列表推导一样,只是要用小括号来代替中括号:
>>>squares=(n*nforninrange(3)) >>>foriinsquares: printi 0 1 4
生成器表达式不会创建序列形式的对象,不会把所有的值都读取到内存中,而是会创建一个通过迭代并按照需求生成值的生成器对象(Generator)。
那么,还有没有其它方法来产生生成器呢?
例子:斐波那契数列
例如有个需求,要生成斐波那契数列的前10位,我们可以这样写:
deffib(n): result=[] a=1 b=1 result.append(a) foriinrange(n-1): a,b=b,a+b result.append(a) returnresult if__name__=='__main__': printfib(10)
数字很少时,函数运行良好,但数字很多时,问题就来了,显然生成一个几千几万长度的列表并不是一个很好的主意。
这样,需求就变成了:写一个可以生成可迭代对象的函数,或者说,不要让函数一次返回全部的值,而是一次返回一个值。
这好像与我们的常识相违背,当我们调用一个普通的Python函数时,一般是从函数的第一行代码开始执行,结束于return语句、异常或者函数结束(可以看作隐式的返回None):
deffib(n): a=1 b=1 foriinrange(n-1): a,b=b,a+b returna if__name__=='__main__': printfib(10) >>> 1 #返回第一个值时就卡住了
函数一旦将控制权交还给调用者,就意味着全部结束。函数中做的所有工作以及保存在局部变量中的数据都将丢失。再次调用这个函数时,一切都将从头创建。函数只有一次返回结果的机会,因而必须一次返回所有的结果。通常我们都这么认为的。但是,如果它们并非如此呢?请看神奇的yield:
deffib(n): a=1 yielda b=1 foriinrange(n-1): a,b=b,a+b yielda if__name__=='__main__': foriinfib(10): printi >>> 1 1 2 3 5 8 13 21 34
生成器Generator
python中生成器的定义很简单,使用了yield关键字的函数就可以称之为生成器,它生成一个值的序列:
defcountdown(n): whilen>0: yieldn n-=1 if__name__=='__main__': foriincountdown(10): printi
生成器函数返回生成器。要注意的是生成器就是一类特殊的迭代器。作为一个迭代器,生成器必须要定义一些方法,其中一个就是__next__()。如同迭代器一样,我们可以使用next()函数(Python3是__next__())来获取下一个值:
>>>c=countdown(10) >>>c.next() 10 >>>c.next() 9
每当生成器被调用的时候,它会返回一个值给调用者。在生成器内部使用yield来完成这个动作。为了记住yield到底干了什么,最简单的方法是把它当作专门给生成器函数用的特殊的return。调用next()时,生成器函数不断的执行语句,直至遇到yield为止,此时生成器函数的”状态”会被冻结,所有的变量的值会被保留下来,下一行要执行的代码的位置也会被记录,直到再次调用next()继续执行yield之后的语句。
next()不能无限执行,当迭代结束时,会抛出StopIteration异常。迭代未结束时,如果你想结束生成器,可以使用close()方法。
>>>c.next() 1 >>>c.next() StopIteration >>>c=countdown(10) >>>c.next() 10 >>>c.close() >>>c.next() StopIteration
协程与yield表达式
yield语句还有更给力的功能,作为一个语句出现在赋值运算符的右边,接受一个值,或同时生成一个值并接受一个值。
defrecv(): print'Ready' whileTrue: n=yield print'Go%s'%n >>>c=recv() >>>c.next() Ready >>>c.send(1) Go1 >>>c.send(2) Go2
以这种方式使用yield语句的函数称为协程。在这个例子中,对于next()的初始调用是必不可少的,这样协程才能执行可通向第一个yield表达式的语句。在这里协程会挂起,等待相关生成器对象send()方法给它发送一个值。传递给send()的值由协程中的yield表达式返回。
协程的运行一般是无限期的,使用方法close()可以显式的关闭它。
如果yield表达式中提供了值,协程可以使用yield语句同时接收和发出返回值。
defsplit_line(): print'readytosplit' result=None whileTrue: line=yieldresult result=line.split() >>>s=split_line() >>>s.next() readytosplit >>>s.send('123') ['1','2','3'] >>>s.send('abc') ['a','b','c']
注意:理解这个例子中的先后顺序非常重要。首个next()方法让协程执行到yieldresult,这将返回result的值None。在接下来的send()调用中,接收到的值被放到line中并拆分到result中。send()方法的返回值就是下一条yield语句的值。也就是说,send()方法可以将一个值传递给yield表达式,但是其返回值来自下一个yield表达式,而不是接收send()传递的值的yield表达式。
如果你想用send()方法来开启协程的执行,必须先send一个None值,因为这时候是没有yield语句来接受值的,否则就会抛出异常。
>>>s=split_line() >>>s.send('123') TypeError:can'tsendnon-Nonevaluetoajust-startedgenerator >>>s=split_line() >>>s.send(None) readytosplit
使用生成器与协程
乍看之下,如何使用生成器和协程解决实际问题似乎并不明显。但在解决系统、网络和分布式计算方面的某些问题时,生成器和协程特别有用。实际上,yield已经成为Python最强大的关键字之一。
比如,要建立一个处理文件的管道:
importos,sys defdefault_next(func): defstart(*args,**kwargs): f=func(*args,**kwargs) f.next() returnf returnstart @default_next deffind_files(target): topdir=yield whileTrue: forpath,dirname,filelistinos.walk(topdir): forfilenameinfilelist: target.send(os.path.join(path,filename))
@default_next defopener(target): whileTrue: name=yield f=open(name) target.send(f) @default_next defcatch(target): whileTrue: f=yield forlineinf: target.send(line) @default_next defprinter(): whileTrue: line=yield printline