一. 讓協(xié)程返回值
下面的例子,我們再次改版之前計算平均值的協(xié)程函數(shù)押赊,這一版本的協(xié)程函數(shù)每次被激活時饺藤,不會自動產(chǎn)出平均值包斑,而是在最后返回一個值。
from collections import namedtuple
Result = namedtuple('Result', 'count average')
def average():
total = 0.0
count = 0
average = None
while True:
term = yield
if term is None:
break
total += term
count += 1
average = total / count
return Result(count, average)
為了返回值涕俗,協(xié)程必須正常終止罗丰,因此 average
中有個判斷條件,以便退出累計循環(huán)再姑。返回值為 namedtuple
萌抵,包含 count
和 average
兩個字段。
注:Python3.3 之前如果生成器返回值元镀,解釋器會報句法錯誤绍填。
演示 1 發(fā)送 None
會終止循環(huán),導(dǎo)致協(xié)程結(jié)束
注意:協(xié)程結(jié)束栖疑,協(xié)程對象會拋出 StopIteration
異常讨永。return
表達(dá)式的值通過異常對象 StopIteration
傳遞給調(diào)用方。這樣做有點不合常理遇革,但是能保留生成器對象的常規(guī)行為卿闹,即在耗盡時拋出 StopIteration
異常。
演示 2 獲取協(xié)程 return
的值
捕獲 StopIteration
異常萝快,并通過異常對象的 value
屬性獲取 average
返回的值锻霎。
下面我們介紹的 yield from
結(jié)構(gòu)會在內(nèi)部自動捕獲 StopIteration
異常,這種處理方式與 for
循環(huán)處理 StopIteration
異常的方式一樣:循環(huán)機制使用用戶易于理解的方式處理異常杠巡。
對 yield from
結(jié)構(gòu)來說量窘,解釋器不僅會捕獲 StopIteration
異常,還會把 value
屬性的值變?yōu)?yield from
表達(dá)式的值氢拥。
二. 使用 yield from
首先要明確蚌铜,yield from
是全新的語法結(jié)構(gòu),其功能要比 yield
豐富嫩海。
注:在
Python 3.5
以后的版本中使用async
冬殃、await
關(guān)鍵字取代yield from
,雖然語法上略有差異叁怪,但是原理是一樣审葬。理解了yield from
,那么async
和await
的使用自然水到渠成奕谭。
演示 1 yield from
可用于簡化 for
循環(huán)中的 yield
表達(dá)式
演示 2 使用 yield from
鏈接可迭代對象
yield from x
表達(dá)式對 x
對象所做的第一件事就是調(diào)用 iter(x)
涣觉,從中獲取迭代器。因此 x
可以是任何可迭代的對象血柳。
如果
yield from
結(jié)果唯一的作用是替代產(chǎn)出值的for
循環(huán)官册,這個結(jié)果可能永遠(yuǎn)不會被添加到 Python 語言中。
yield from
的主要功能是打開雙向通道难捌,把最外層的調(diào)用方與最內(nèi)層的子生成器連接起來膝宁,這樣二者可以直接發(fā)送和產(chǎn)出值鸦难,還可以直接傳入異常,而不用在位于中間的協(xié)程中添加大量處理異常的樣板代碼员淫。
在舉例前合蔽,我們先來了解幾個專門的術(shù)語:
- 委派生成器:包含
yield from <iterable>
表達(dá)式的生成器函數(shù)。 - 子生成器:從
yield from
表達(dá)式中<iterable>
部分獲取的生成器介返。 - 調(diào)用方:調(diào)用委派生成器的客戶端代碼拴事。在適當(dāng)?shù)恼Z境中,將使用“客戶端”代指“調(diào)用方”映皆,以便于委派生成器(也是調(diào)用方挤聘,因為它調(diào)用了子生成器)區(qū)分開。
委派生成器在 yield from
表達(dá)式處暫停時捅彻,調(diào)用方可以直接把數(shù)據(jù)發(fā)給子生成器,子生成器再把產(chǎn)出的值發(fā)給調(diào)用方鞍陨。子生成器返回之后步淹,解釋器會拋出 StopIteration
異常,并把返回的值附加到異常對象上诚撵,此時委派生成器會恢復(fù)缭裆。
示例 使用 yield from
計算平均值,并輸出統(tǒng)計報告
from collections import namedtuple
Result = namedtuple('Result', 'count, average')
# 子生成器
def average():
total = 0.0
count = 0
average = None
while True:
term = yield
if term is None:
break
total += term
count += 1
average = total/count
return Result(count, average)
# 委派生成器
def grouper(results, key):
while True:
results[key] = yield from average()
print(results[key])
# 客戶端代碼寿烟,即調(diào)用方
def main(data):
results = {}
for key, values in data.items():
group = grouper(results, key)
next(group)
for value in values:
group.send(value)
group.send(None)
report(results)
# 輸出報告
def report(results):
print('-'*20, 'report', '-'*20)
for key, result in sorted(results.items()):
group, unit = key.split(';')
print(f'{result.count:2} {group:5} averaging {result.average:.2f}{unit}')
data = {
'girls;kg': [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
'girls;m': [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
'boys;kg': [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
'boys;m': [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46], }
if __name__ == '__main__':
main(data)
運行結(jié)果:外層 for
循環(huán)每次迭代會產(chǎn)生一個 grouper
實例澈驼,賦值給 group
變量,group
是委派生成器筛武。
調(diào)用 next(group)
缝其,預(yù)激委派生成器 group
,此時進(jìn)入 while
循環(huán)徘六,調(diào)用子生成器 average
后内边,在 yield from
表達(dá)式處暫停。內(nèi)層 for
循環(huán)調(diào)用 group.send(value)
待锈,直接把值傳給子生成器 average
漠其。同時,當(dāng)前的 group
在 yield from
表達(dá)式處暫停竿音。
內(nèi)層循環(huán)結(jié)束后和屎,group
實例依舊在 yield from
表達(dá)式處暫停。因此春瞬,grouper
函數(shù)定義體中為 results[key]
賦值的語句還沒有執(zhí)行柴信。如果外層 for
循環(huán)的末尾沒有 group.send(None)
,那么 average
子生成器永遠(yuǎn)不會終止快鱼,委派生成器 group
永遠(yuǎn)不會再次激活颠印,因此永遠(yuǎn)不會為 results[key]
賦值纲岭。
外層 for
循環(huán)重新迭代時會新建一個 grouper
實例,然后綁定到 group
變量上线罕。前一個 grouper
實例止潮,以及它創(chuàng)建的尚未終止的 average
子生成器實例,被垃圾回收機制回收钞楼。
注意:
-
group.send(None)
以及average
循環(huán)中的條件判斷是至關(guān)重要的終止條件喇闸。如果不這么做,使用yield from
調(diào)用這個協(xié)程的生成器會永遠(yuǎn)阻塞询件。 - 返回的
Result
會成為grouper
函數(shù)中yield from
表達(dá)式的值燃乍。 -
grouper
是委派生成器; - 這個循環(huán)每次迭代時會新建一個
average
實例宛琅,每個實例都作為協(xié)程使用的生成器對象刻蟹。 -
grouper
發(fā)送的每個值都會經(jīng)由yield from
處理,通過管道傳給average
實例嘿辟。group
會在yield from
表達(dá)式處暫停舆瘪,等待average
實例處理客戶端發(fā)來的值。average
實例運行完畢后红伦,返回的值綁定到results[key]
上英古。while
循環(huán)會不斷創(chuàng)建average
實例,處理更多的值昙读。 - 把各個
value
傳給grouper
召调,傳入的值最終到達(dá)averager
函數(shù)中term = yield
那一行;grouper
永遠(yuǎn)不知道傳入的值是什么蛮浑。 - 把
None
傳入grouper
唠叛,導(dǎo)致當(dāng)前的averager
實例終止,也讓grouper
繼續(xù)運行陵吸,再創(chuàng)建一個averager
實例玻墅,處理下一組值。
這個實驗想表名的關(guān)鍵一點是壮虫,如果子生成器不終止澳厢,委派生成器會在 yield from
表達(dá)式處永遠(yuǎn)暫停。如果是這樣囚似,程序不會向前執(zhí)行剩拢,因為 yield from
把控制權(quán)交給客戶代碼(即委派生成器的調(diào)用方)了。
yield from
的意義
- 子生成器產(chǎn)出的值都直接傳給委派生成器的調(diào)用方饶唤。
- 使用
send
方法發(fā)給委派生成器的值都直接傳給子生成器徐伐。如果發(fā)送的值是None
,那么會調(diào)用子生成器的__next__
方法募狂。如果發(fā)送的值不是None
办素,那么會調(diào)用子生成器的send
方法角雷。如果調(diào)用的方法拋出StopIteration
異常,那么委派生成器恢復(fù)運行性穿。任何其它異常都會向上冒泡勺三,傳給委派生成器。 - 生成器退出時需曾,生成器中的
return expr
表達(dá)式會觸發(fā)StopIteration
異常拋出吗坚。 -
yield from
表達(dá)式的值是子生成器終止時傳給StopIteration
異常的第一個參數(shù)。
關(guān)于 yield
實現(xiàn)生成器的介紹至此已經(jīng)完成呆万,下一節(jié)我們將使用 asyncio
來實現(xiàn)協(xié)程商源。