python中的yield, yield from之最详最强解释,一看就懂(一)
yield是python中一个非常重要的关键词,所有迭代器都是yield实现的,学习python,如果不把这个yield的意思和用法彻底搞清楚,学习python的生成器,协程和异步io的时候,就会彻底懵逼。所以写一篇总结讲讲yield的东西。
分成两块来讲, 这篇先说yield基本用法
SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。一, 生成器中使用yield
语法形式:yield <表达式>
这种情况,可以简单的把它理解为 return <表达式>, 每次next调用,会触发生成器从yield后面一条语句开始, 继续执行, 知道下一次遇到yield。 比如下面的代码
1 def generator(): 2 print("gen-0: start") 3 for i in range(2): 4 print("gen-%s-a: i=%s" % (i,i)) 5 yield i + 1 # 可简单理解为 return i + 1, 并等待 6 print("gen-%s-b: i=%s" % (i,i)) 7 print("gen-c") 8 yield 3 # 可简单理解为 return 3, 并等待 9 print("gen-d") # 此处继续执行完成后,由于后续在无其它yield语句,调用next的代码会抛出StopIteration异常 10 11 try: 12 g = generator() # 由于generator是一个包含yield关键字的生成器,此处直接调用gengerator()只是返回该生成器实例,实际无任何输出 13 print('main-0: start') 14 n = next(g) # next触发生成器执行,由于是第一次触发,所以从generator循环外第2行和第4行的打印会输出,到第5行遇到yield后返回i+1到主程序,第6行不会执行 15 print('main-1: n=%s' % n) 16 n = next(g) # next第二次触发生成器执行,由于是第二次触发,上次执行到第5行, 此时从第6行开始执行, 将输出第6行和第4行的打印, 并再次在第5行返回 17 print('main-2: n=%s' % n) 18 n = next(g) # next第三次触发生成器执行,这次generator内部for循环已经完成,故执行完跳出循环执行到第8行后返回 19 print('main-3: n=%s' % n) 20 n = next(g) # next视图第四次触发生成器执行,从第9行开始执行,但此后generator已经全部执行完成,所以此时next会抛出StopIteration异常,赋值无法完成 21 print('main-5: n=%s' % n) # 异常产生, 此行将永远不会被执行 22 except StopIteration: 23 print('StopIteration n=%s' % n) # 第17行的next将触发异常
上面这段代码执行输出如下, 各位看官自行对照代码和详细注释很容易看懂
main-0: start
gen-0: start
gen-0-a: i=0
main-1: n=1
gen-0-b: i=0
gen-1-a: i=1
main-2: n=2
gen-1-b: i=1
gen-c
main-3: n=3
gen-d
StopIteration n=3
二, 可以接收传入参数值的yield
语法形式:x = yield <表达式>
next方式触发生成器无法传入参数, 如果想触发generator的同时, 传入参数给生成器,是否可以喃?答案是可以的, 这时候需要做两个改变,一是在生成器内增加变量接收该参数, 即本届标题形式;二是外部调用时, 不使用next, 而采用用调用生成器的send方法即可
1 def coroutine(): # 这个生成器,我们其名coroutine, 意思是协程, 因为有yield, 所以本质上协程也是一个生成器 2 print("coroutine: start") 3 for i in range(2): 4 print("coroutine-a: i=%s" % i) 5 x = yield i + 1 # 由send传入的参数值将被赋值给x, 注意i+1的值是被yield用来返回的,不会赋值给x ! 6 print("coroutine-b: i=%s, x=%s" % (i,x)) 7 8 cr = coroutine() # 创建一个生成器实例 9 next(cr) # 生成器的第一次触发必须使用next,此时如果试图用send发送参数,将导致异常败,原因是此时生成器还未启动 10 try: 11 print("main-a:") 12 y = cr.send(0) # 调用生成器的send方法, 将循环变量i的值传给生成器, 并触发一次生成器的执行 13 print("main-b: y=%s" % y) 14 y = cr.send(1) # 调用生成器的send方法, 将循环变量i的值传给生成器, 并触发一次生成器的执行 15 print("main-c: y=%s" % y) # 14行的消息发送将导致StopIteration异常产生, 此行将永远不会被执行 16 except StopIteration: 17 print("StopIteration")
以下是执行后的输出
coroutine: start coroutine-a: i=0 main-a: coroutine-b: i=0, x=0 coroutine-a: i=1 main-b: y=2 coroutine-b: i=1, x=1 StopIteration
简单再解释一下,
1. 第一次执行next时,生成器通过yield i + 1 返回1, 并停在第5行等待被唤醒, 此时赋值动作尚未发生, x的值只是个None, 同时也为执行后面的print。
2. 生成器通过next进行过首次触发后,可以用send发送参数,比如第11行和12行,生成器内部变量先分别被赋值为 0和1, 大家可以对照输出看
3. 由于生成器总共只有两次yield的机会, next消耗一次,第一次send消耗一次,所以第二次send后,虽然不影响执行完参数传递给生成器的动作, 但由于生成器自身找不下一次yield的机会,生成器执行终止。 导致send抛出StopIteration异常
三。生成器本身的返回值
不知道大家注意到没有, 直接类似的cr = coroutine() 的调用,只是产生一个生成器实例,并没执行。而通过yield的返回时, 生成器本身的逻辑实际上都是为走完的, yield英文是退让的意思,除了无限序列的生成器,生成器最终都会执行完成,那么此时如果生成器通过return正常返回一个值,生成器的使用者能获得吗 ?比如下面的代码, 生成器正常结束后return的代码, 使用方如何获得喃 ?比如下面代码如何获取第10行正常终止后的返回值10
1 def generator(): 2 yield 5 3 return 10 # 这个返回值如何获得 ? 4 5 cr = generator() 6 n = next(cr) # yield 5返回的值可以被获得并赋值给n
答案是在StopIteration异常的处理逻辑中可以获得,方法如下, 只需要在上面代码后面继续加上以下处理
try: next(cr) except StopIteration as e: # 通过except ... as 将异常保存在变量e中 rt = e.value # 异常变量e的value值就是生成器通过return返回值
严格意义上讲, python将StopIteration定义为一个异常可能是不得已的事情, 因为这个异常的意思, 实际就是告诉使用者, “我没发生成下一个了, yield次数已经用完了, 我的使命结束了”, 从这个意义上讲, StopIteration也可以归为是正常逻辑, 所以强烈建议所有使用用生成器的地方,都应该要加上StopIteration异常处理。
下一篇 讲yield from 相关用法(欢迎关注,即将更新)
