看到協(xié)程時(shí)對(duì)yield的用法總是理解不夠透徹,因此做一些小筆記,方便日后查看。
此處以一個(gè)小例子來說明send到底是干嘛用的,例子來自Python協(xié)程:從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
#簡單調(diào)用:
for i in fib(5):
print(i,end=' ',sep=',')
# 1 1 2 3 5
#相當(dāng)于
f = fib(5)
for j in range(5):
print(next(f),end=' ')
在上面的例子中,函數(shù)fib(n)相當(dāng)于一個(gè)生成器,for循環(huán)每一次調(diào)用相當(dāng)于執(zhí)行一次next(),最后調(diào)用完會(huì)遇到StopIteration退出for循環(huán)。
2、send是什么?
想要了解send()是什么,就不得不先理解yield表達(dá)式了。yield表達(dá)式可表示為[res =] yield [expression],從某種程度來說,方括號(hào)中的值都是可以省略的,例如若將fib(n)函數(shù)的yield n改成yield也不會(huì)報(bào)錯(cuò),只是這樣fib(5)這個(gè)生成器就會(huì)返回5個(gè)None了,而將yield n改成 s = yield n結(jié)果則不會(huì)變,只是此時(shí)可以通過send向yield表達(dá)式傳入數(shù)值,這個(gè)數(shù)值即賦值給了s??纯创a更加清晰:
# 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表達(dá)式后對(duì)生成器沒有改變,那么他有什么作用呢?要想yield表達(dá)式發(fā)揮作用,就必須使用send對(duì)其進(jìn)行傳值(賦值),利用傳入的值可以來實(shí)現(xiàn)一些有用的功能,例如下面簡單的記錄一下日志信息:
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) #這一步是必須的,此處相當(dāng)于send(None),在fib函數(shù)中此時(shí)執(zhí)行到y(tǒng)ield產(chǎn)出值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發(fā)送的值都賦值給了yield表達(dá)式的左邊的now變量了。另外值得注意的一點(diǎn)是,在使用send之前必須先調(diào)用一次next(),此處的next相當(dāng)于send(None)。
yield表達(dá)式的執(zhí)行順序是先yield產(chǎn)生值,然后掛起等待send傳入值。也因此輸出的結(jié)果是先輸出fib序列,然后在輸出傳入值相關(guān)的信息。
下面的例子更好的說明了執(zhí)行步驟,為了更好的說明執(zhí)行順序,此處將fib序列的第一個(gè)值改成了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
#此時(shí)僅執(zhí)行了一次send,輸出如下
3
2017-12-21 21:23:06.106728
5
上面的兩個(gè)不同的fib生成結(jié)果中第一個(gè)3是在預(yù)激活協(xié)程時(shí)yield產(chǎn)生的,在yield表達(dá)式右邊產(chǎn)生值后,便會(huì)掛起等待傳入?yún)?shù)并賦值給左側(cè)的變量now。隨后send將時(shí)間傳入賦值給了yield 表達(dá)式左邊的now,now被賦值后會(huì)一直執(zhí)行到再次yield b生成5,這也是為什么下面的res是5。此時(shí)yield 表達(dá)式又再次執(zhí)行到了yield并掛起等待給now賦值(send)的時(shí)候。如此循環(huán),直到yield表達(dá)式右側(cè)(這里的右側(cè)依舊是一個(gè)生成器)的值耗盡,這是再次send時(shí)會(huì)引發(fā)生StopIteration。
3、yield from 是何方神圣?
說完了send,yield from又是用來干什么的呢?下面這個(gè)例子也許可以對(duì)yield from的用法做一些最簡單的說明:
def f1():
for i in range(5):
yield i
for j in 'abc':
yield j
def f2():
yield from f1()
# 下面兩種調(diào)用生成器的結(jié)果是一致的
for i in f1():
print(i,end=' ')
for j in f2():
print(j,end=' ')
# 0 1 2 3 4 a b c
但是yield from的作用僅僅如此嗎?
你見過有返回值的生成器嗎?下面這個(gè)生成器在終止時(shí)(觸發(fā)StopIteration)會(huì)返回一個(gè)值。
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的值發(fā)送到了子生成器func()中
except:
pass
i += 1
#輸出
s: 10
s: 11
s: 12
s: 13
s: 14
res: 111
從輸出結(jié)果可以看出3件事:
首先,委派生成器delegate的確從yield from中收到了返回值——res=111,而且這個(gè)返回值并非yield的值,而是子生成器函數(shù)func的返回值。
其次,從輸出結(jié)果可以看出,send發(fā)送的值都傳到了子生成器中,也即是從委派生成器delegate傳到了yield from表達(dá)式中的子生成器func中,這也是輸出結(jié)果index 10 ... index 14的由來。
最后,委派生成器向子生成器send發(fā)送值后,自身會(huì)被掛起,直到子生成器函數(shù)func觸發(fā)終止異常(StopIteration)返回值,這個(gè)返回值賦值給yield from表達(dá)式左邊的res,然后委派生成器就會(huì)繼續(xù)執(zhí)行,這也是為什么,res:111會(huì)在最后輸出。當(dāng)我們將while i<15改成while i<12后會(huì)看到輸出結(jié)果沒有res輸出,這是因?yàn)樯善鬟€沒有迭代完(while index<5這里需要send5次才會(huì)觸發(fā)異常返回值),還在等待send發(fā)送值。
現(xiàn)在,讓我們梳理下上面代碼的執(zhí)行順序:
①預(yù)激活子生成器func,此時(shí)子生成器在等待傳值;
②向委派生成器delegate傳值(send);
③委派生成器通過yield from表達(dá)式向子生成器func中傳(send)值;
④子生成器收到傳入的值i后,便會(huì)向后執(zhí)行print('s: ', s),index+=1并yield產(chǎn)生值(雖然此處沒有生成任何值),最后又回到繼續(xù)等待傳值(send)的狀態(tài);
⑤代碼中沒有⑤因?yàn)棰荽碇h(huán)傳值這個(gè)過程;
⑥終止生成器,這一步非常關(guān)鍵,因?yàn)槊總魅?send)一個(gè)值后都會(huì)執(zhí)行index+=1,回到等待傳值得狀態(tài)。因此第一次傳值后index=1(需要注意的是預(yù)激活時(shí)index為0),第四次傳值(send(13))后,此時(shí)index=4,當(dāng)?shù)谖宕蝹髦禃r(shí)index=5此時(shí)會(huì)觸發(fā)異常退出while循環(huán),使得子生成器func返回res值,func返回的值又通過yield from表達(dá)式賦值給委派生成器的res,res收到值后委派生成器終于不再掛起,向下執(zhí)行,print(res)。完結(jié)撒花。