看到协程时对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
这里需要send
5次才会触发异常返回值),还在等待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)
。完结撒花。