python中的生成器及yield
在实际开发中受到内存限制,列表容量肯定是有限的
而且,创建一个包含太多个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。
所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间
在Python中,这种一边循环一边计算的机制,称为生成器:generator
要创建一个generator,有三种方法
方法一、只要把一个列表生成式的[]改成():
>>>g=(x*x for x in range(10))
>>>g
<generatorobject<genexpr>at0x1022ef630>
方法二、iter()函数用于将可迭代对象如list、tuple、dict、set、str等转换为生成器对象
方法三、如果推算的算法比较复杂,用类似列表生成式无法实现的时候,可以在实现算法的函数中使用yield表达式来实现
一个带有yield表达式的函数就是一个生成器generator,当调用这个函数时不会执行任何函数代码只返回一个生成器对象
yield是一个表达式,是表达式就有返回值,yield关键字后面的值是返回给调用者的值而并不是表达式的返回值,表达式的返回值需要我们主动传入
当生成器执行到yield表达式时会返回yield后面的值并被动的等待我们传入yield表达式的值(对外表现为中断),当传入yield表达式的值后程序会向下继续运行
生成器对象的send()与next()方法用于传递yield表达是的值,区别在于send传递的值可以由我们指定,而next只会传递None值,所以send(None)等同于next()
生成器使用特点
第一次运行生成器对象时因为没有yield表达式运行过,所以没有赋值的目标,此时必须传入None值,也就是第一次运行时只能使用send(None)或next()
一般第一次运行next()或者send(None)我们称之为激活生成器
一个generator执行结束会抛出StopIteration(expr)异常,其中expr为return的值,该错误对象的value属性为expr值
在for循环中会自动调用next()并返回生成器的返回值,直到遇到StopIteration错误时结束循环
def demo(msg):
print("starting...,msg:"+msg)
while True:
val=yield msg
print(f"msg:{msg},val:{val}")
if val=='exit':
return "end"
test=demo('one')
#函数中有yield关键字,所以函数并不会真的执行,而是得到一个生成器对象并赋值给变量test
next(test)
#运行next()运行函数体并运行至yield处中断,此时程序已经输出"start,msg:one"
#此时已经运算完成yield表达式,生成器返回msg的值"one"
val=test.send('two')
#运行send()传入值"two",此时变量val赋值为send()传入值"two"
#继续向下运行输出:"msg:one,val:two"
#继续向下运行知道下次循环再次遇到yield关键字
print('生成器返回值:'+val)
#生成器返回值为msg变量的值
try:
test.send('exit')
except StopIteration as e:
print('返回值:',e.value)
#继续向下运行程序直到再次遇到yield关键字重复以上步骤或遇到return语句结束
综合以上代码的运行结果为
starting...,msg:one
msg:one,val:two
生成器返回值:one
msg:one,val:exit
返回值:end
上例中我们认识了生成器的next()和send()两个方法,生成器还有两个重要的方法:close()和throw()方法
close()方法用于手动关闭生成器,后续调用会直接返回StopIteration异常
生成器的throw(error)方法,用于在生成器中抛出指定异常,如果在生成器中捕获了该异常,throw方法会返回下一次迭代值,否则throw方法返回指定的异常
def my_gen():
yield "a"
yield "b"
g=my_gen()
print(next(g)) # a
print(g.throw(ValueError("throw方法抛出的异常")))
#抛出ValueError异常
#使用了g.throw(ValueError),而生成器中并没有捕获ValueError异常,所以生成器结束运行,throw方法返回该异常
defmy_gen():
try:
yield"a"
yield"b"
except ValueError as e:
print("生成器中捕获了该异常")
g=my_gen()
print(next(g)) # a
print(g.throw(ValueError("throw方法抛出的异常")))
#抛出StopIteration异
#由于生成器中捕获了指定异常,生成器会继续执行,但是由于当前生成器已经执行到最后,所以会抛出StopIteration异常
defmy_gen():
whileTrue:
try:
yield"a"
yield"b"
except ValueError as e:
print("生成器中捕获了该异常")
g=my_gen()
print(next(g)) # a
print(g.throw(ValueError("throw方法抛出的异常"))) # a
# 生成器捕获了指定异常,生成器继续执行,在yield"a"表达式处暂停并返回"a",此时throw方法的返回值为"a"
生成器的GeneratorExit异常
当一个生成器被销毁时会抛出GeneratorExit异常后再销毁,也就是说当一个生成器执行完毕或通过del语句或调用close()主动销毁生成器对象时会抛出该异常
GeneratorExit异常只有在生成器对象被激活后,才有可能产生,未激活时即使使用del语句主动销毁也不会抛出GeneratorExit异常
defgen():
try:
yield"a"
yield"b"
except GeneratorExit:
print('生成器销毁了')
g=gen()
next(g)
del g
#打印"生成器销毁了"
显式的捕获了GeneratorExit异常,如果该异常没有被显式捕获,生成器对象也不会把该异常向主程序抛出
因为GeneratorExit异常定义的初衷,是方便开发者在生成器对象调用结束后定义一些收尾的工作,如释放资源等
迭代器
我们已经知道,可以直接作用于for循环的数据类型有以下几种:
一类是集合数据类型,如list、tuple、dict、set、str等
一类是生成器generator
这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。
可以使用isinstance()判断一个对象是否是Iterable对象
from collections import Iterable
isinstance([],Iterable)
可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator
可以使用isinstance()判断一个对象是否是Iterator对象
from collections import Iterator
isinstance((xforxinrange(10)),Iterator)
生成器都是Iterator对象,但list、dict、str虽然是Iterable,却不是Iterator
yield from表达式
yield from是在Python3.3才出现的语法,后面是可迭代对象Iterable
在使用yield表达式的时候,使用for语句迭代是自动捕获StopIteration,如果遇到return只是会终止迭代,没办法获取最后return的值
yield from也是一个表达式,那么也有返回值,该返回值就是最后的return值,我们可以获取该return值
def my_gen():
for i in range(5):
if i==2:
return '中断了'
yield i
def wrap_gen(gen):
res=yield from gen
yield res
def main(gen):
for j in gen:
print(j)
wrap_g=wrap_gen(my_gen())
main(wrap_g)
以上代码会输出
0
1
中断了
其中最后一行的"中断了"就是wrap_gen中的yieldres表达式返回值,也就是my_gen中的return值
在PEP 380使用了一些专门术语
委派生成器:包含yield from
子生成器:从yield from表达式中
调用方:调用委派生成器的代码;即示例中main函数
委派生成器会在yield from表达式处暂停,此时调用方可以直接把数据发给子生成器,子生成器的返回值也是直接返回给调用方
委派生成器相当于在调用方与子生成器之间建立一个双向通道,它本身并没有权利也没有办法对子生成器yield回来的内容做拦截
当子生成器抛出异常时(如运行结束抛出StopIteration异常或者其他情况下抛出其他异常)并不会抛出给调用方,而是抛给委派生成器,此时委派生成器会恢复执行,并捕获异常中的第一个参数值赋值给yield from表达式
yield from x 表达式内部首先是调用iter(x),然后再调用next(),因此x是任何的可迭代对象
可以把任意数量的委派生成器连接在一起,一个委派生成器使用yield from调用一个子生成器,而子生成器本身也可以是委派生成器,使用yield from调用另一个生成器