看到协程时对yield的用法总是理解不够透彻,因此做一些小笔记,方便日后查看。
此处以一个小例子来说明send到底是干嘛用的,例子来自Python协程:从yield/send到async/await:
1、起源:简单的yield生成器
def fib(n):
index = a = 0
b = 1
while index<n:
yield b
a, b = b, a+b
index += 1
#简单调用:
for i in fib(5):
print(i,end=' ',sep=',')
# 1 1 2 3 5
#相当于
f = fib(5)
for j in range(5):
print(next(f),end=' ')
在上面的例子中,函数fib(n)相当于一个生成器,for循环每一次调用相当于执行一次next(),最后调用完会遇到StopIteration退出for循环。
2、send是什么?
想要了解send()是什么,就不得不先理解yield表达式了。yield表达式可表示为[res =] yield [expression],从某种程度来说,方括号中的值都是可以省略的,例如若将fib(n)函数的yield n改成yield也不会报错,只是这样fib(5)这个生成器就会返回5个None了,而将yield n改成 s = yield n结果则不会变,只是此时可以通过send向yield表达式传入数值,这个数值即赋值给了s。看看代码更加清晰:
# yield b ==> yield
def fib(n):
index = a = 0
b = 1
while index<n:
yield
a, b = b, a+b
index += 1
for i in fib(5):
print(i,end=' ')
# None None None None None
# yield b ==> s=yield b
def fib(n):
index = a = 0
b = 1
while index<n:
s = yield b
a, b = b, a+b
index += 1
# 1 1 2 3 5
既然使用了yield表达式后对生成器没有改变,那么他有什么作用呢?要想yield表达式发挥作用,就必须使用send对其进行传值(赋值),利用传入的值可以来实现一些有用的功能,例如下面简单的记录一下日志信息:
import datetime
import time
import random
def fib(n):
index = a = 0
b = 1
while index < n:
now = yield b
print(now)
a, b = b, a+b
index += 1
f = fib(5)
res = next(f) #这一步是必须的,此处相当于send(None),在fib函数中此时执行到yield产出值b=1(也即res等于1),并挂起等待send传入值
while True:
try:
print(res)
time.sleep(random.random())
res = f.send(datetime.datetime.now())
except:
print('over')
break
#输出
1
2017-12-21 14:15:49.296765
1
2017-12-21 14:15:49.583339
2
2017-12-21 14:15:50.492255
3
2017-12-21 14:15:51.006442
5
2017-12-21 14:15:51.113537
over
从上面可以看出,send发送的值都赋值给了yield表达式的左边的now变量了。另外值得注意的一点是,在使用send之前必须先调用一次next(),此处的next相当于send(None)。
yield表达式的执行顺序是先yield产生值,然后挂起等待send传入值。也因此输出的结果是先输出fib序列,然后在输出传入值相关的信息。
下面的例子更好的说明了执行步骤,为了更好的说明执行顺序,此处将fib序列的第一个值改成了2:
import datetime
import time
import random
def fib(n):
index = 0
a = 2
b = 3
while index < n:
now = yield b
print(now)
a, b = b, a+b
index += 1
f = fib(5)
res = next(f)
n = 1
while n < 2:
try:
print(res)
time.sleep(random.random())
res = f.send(datetime.datetime.now())
print(res)
except:
print('over')
break
n += 1
#此时仅执行了一次send,输出如下
3
2017-12-21 21:23:06.106728
5
上面的两个不同的fib生成结果中第一个3是在预激活协程时yield产生的,在yield表达式右边产生值后,便会挂起等待传入参数并赋值给左侧的变量now。随后send将时间传入赋值给了yield 表达式左边的now,now被赋值后会一直执行到再次yield b生成5,这也是为什么下面的res是5。此时yield 表达式又再次执行到了yield并挂起等待给now赋值(send)的时候。如此循环,直到yield表达式右侧(这里的右侧依旧是一个生成器)的值耗尽,这是再次send时会引发生StopIteration。
3、yield from 是何方神圣?
说完了send,yield from又是用来干什么的呢?下面这个例子也许可以对yield from的用法做一些最简单的说明:
def f1():
for i in range(5):
yield i
for j in 'abc':
yield j
def f2():
yield from f1()
# 下面两种调用生成器的结果是一致的
for i in f1():
print(i,end=' ')
for j in f2():
print(j,end=' ')
# 0 1 2 3 4 a b c
但是yield from的作用仅仅如此吗?
你见过有返回值的生成器吗?下面这个生成器在终止时(触发StopIteration)会返回一个值。
def func():
index = 0
res = 111
while index < 5:
s = yield # ④
print('s: ', s)
index += 1
return res
def delegate():
res = yield from func() # ③ ##⑥
print('res: ', res)
f = delegate()
f.send(None) # ①
i = 10
while i < 15:
try:
f.send(i) # ② 此处send的值发送到了子生成器func()中
except:
pass
i += 1
#输出
s: 10
s: 11
s: 12
s: 13
s: 14
res: 111
从输出结果可以看出3件事:
首先,委派生成器delegate的确从yield from中收到了返回值——res=111,而且这个返回值并非yield的值,而是子生成器函数func的返回值。
其次,从输出结果可以看出,send发送的值都传到了子生成器中,也即是从委派生成器delegate传到了yield from表达式中的子生成器func中,这也是输出结果index 10 ... index 14的由来。
最后,委派生成器向子生成器send发送值后,自身会被挂起,直到子生成器函数func触发终止异常(StopIteration)返回值,这个返回值赋值给yield from表达式左边的res,然后委派生成器就会继续执行,这也是为什么,res:111会在最后输出。当我们将while i<15改成while i<12后会看到输出结果没有res输出,这是因为生成器还没有迭代完(while index<5这里需要send5次才会触发异常返回值),还在等待send发送值。
现在,让我们梳理下上面代码的执行顺序:
①预激活子生成器func,此时子生成器在等待传值;
②向委派生成器delegate传值(send);
③委派生成器通过yield from表达式向子生成器func中传(send)值;
④子生成器收到传入的值i后,便会向后执行print('s: ', s),index+=1并yield产生值(虽然此处没有生成任何值),最后又回到继续等待传值(send)的状态;
⑤代码中没有⑤因为⑤代表着循环传值这个过程;
⑥终止生成器,这一步非常关键,因为每传入(send)一个值后都会执行index+=1,回到等待传值得状态。因此第一次传值后index=1(需要注意的是预激活时index为0),第四次传值(send(13))后,此时index=4,当第五次传值时index=5此时会触发异常退出while循环,使得子生成器func返回res值,func返回的值又通过yield from表达式赋值给委派生成器的res,res收到值后委派生成器终于不再挂起,向下执行,print(res)。完结撒花。