協(xié)程
to yield 含義:產(chǎn)出和讓步瘩扼。
yield item這行代碼會產(chǎn)出一個值,提供給next(...)的調(diào)用方垃僚;此外集绰,還會作出讓步,暫停執(zhí)行生成器谆棺,讓調(diào)用方繼續(xù)工作栽燕,直到需要使用另一個值時再調(diào)用next()。調(diào)用方會從生成器中拉取值改淑。
從根本上把yield視作控制流程的方式碍岔,這樣就好理解協(xié)程了。
將生成器當作一個協(xié)程
生成器的調(diào)用方可以使用.send(...)方法發(fā)送數(shù)據(jù)朵夏,發(fā)送的數(shù)據(jù)會成為生成器函數(shù)中yield表達式的值蔼啦。因此,生成器可以作為協(xié)程使用仰猖。協(xié)程是指一個過程捏肢,這個過程與調(diào)用方協(xié)作,產(chǎn)出由調(diào)用方提供的值饥侵。
def simple_coroutine():
print("->coroutine started")
x = yield # ①
print("-> coroutine received:", x)
my_coro = simple_coroutine()
next(my_coro) # ②
my_coro.send(42) # ③
# 輸出:
->coroutine started
Traceback (most recent call last):
-> coroutine received: 42
File "C:/Users/42072/PycharmProjects/day01/ready04/yield_reday.py", line 62, in <module>
my_coro.send(42)
StopIteration
- ① yield在表達式中使用鸵赫;如果協(xié)程只需從客戶那里接收數(shù)據(jù),那么產(chǎn)出的值是None——這個值是隱式指定的躏升,因為yield關(guān)鍵字右邊沒有表達式辩棒。
- ② 首先要調(diào)用next(...)函數(shù),因為生成器還沒啟動,沒在yield語句處暫停一睁,所以一開始無法發(fā)送數(shù)據(jù)藕赞。
- ③ 調(diào)用這個方法后,協(xié)程定義體中的yield表達式會計算出42卖局;現(xiàn)在斧蜕,協(xié)程會恢復,一直運行到下一個yield表達式砚偶,或者終止批销。
協(xié)程的狀態(tài)
- GEN_CREATED:等待開始執(zhí)行
- GEN_RUNNING:解釋器正在執(zhí)行
- GEN_SUSPENDED:在yield表達式處暫停
- GEN_CLOSED:執(zhí)行結(jié)束
from inspect import getgeneratorstate
def simple_coroutine():
print("->coroutine started")
x = yield
print(getgeneratorstate(my_coro))
print("-> coroutine received:", x)
my_coro = simple_coroutine()
print(getgeneratorstate(my_coro))
next(my_coro)
print(getgeneratorstate(my_coro))
my_coro.send(42)
print(getgeneratorstate(my_coro))
注意:
- 因為send方法的參數(shù)會成為暫停的yield表達式的值,所以染坯,僅當協(xié)程處于暫停狀態(tài)時才能調(diào)用send方法均芽。
- 如果給未激活的協(xié)程對象發(fā)送None以外的值,會引發(fā)錯誤单鹿。
激活協(xié)程的方式有兩種:
- next(my_coro)方法
- my_coro.send(None)
示例:協(xié)程產(chǎn)出兩個值
def simple_coro2(a):
print('->Started :a = ' ,a)
b = yield a
print('->Started :b = ' ,b)
c = yield a + b
print('->Received: c=',c)
my_coro2 = simple_coro2(14)
next(my_coro2)
rs1 = my_coro2.send(28)
print(rs1)
rs2 = my_coro2.send(99)
print(rs2)
案例:計算移動平均值
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total / count
coro_avg = averager()
next(coro_avg)
print(coro_avg.send(10))
print(coro_avg.send(20))
print(coro_avg.send(30))
#輸出:
10.0
15.0
20.0
使用協(xié)程之前必須預激掀宋,可是這一步容易忘記。為了避免忘記仲锄,可以在協(xié)程上使用一個特殊的裝飾器劲妙。
- 額外知識:wraps
作者:hqzxsc2006
來源:CSDN
原文:https://blog.csdn.net/hqzxsc2006/article/details/50337865
Python裝飾器(decorator)在實現(xiàn)的時候,被裝飾后的函數(shù)其實已經(jīng)是另外一個函數(shù)了(函數(shù)名等函數(shù)屬性會發(fā)生改變)儒喊,為了不影響镣奋,Python的functools包中提供了一個叫wraps的decorator來消除這樣的副作用。寫一個decorator的時候怀愧,最好在實現(xiàn)之前加上functools的wrap侨颈,它能保留原有函數(shù)的名稱和docstring。
#不加wraps
def my_decorator(func):
def wrapper(*args, **kwargs):
"""decorator"""
print("Calling decorated function...")
return func(*args,**kwargs)
return wrapper
@my_decorator
def example():
"""Docstring"""
print('Called example function')
print(example.__name__,example.__doc__)
#輸出:
wrapper decorator
#加wraps
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
"""decorator"""
print("Calling decorated function...")
return func(*args,**kwargs)
return wrapper
@my_decorator
def example():
"""Docstring"""
print('Called example function')
print(example.__name__,example.__doc__)
#輸出:
example Docstring
預激協(xié)程
協(xié)程如果不激活芯义,那么則沒什么用哈垢。調(diào)用send()方法之前,需要先調(diào)用next(my_coro)方法進行激活扛拨。為了簡化協(xié)程的用法耘分,有時會使用一個預激裝飾器。
from functools import wraps
def coroutine(func):
@wraps(func)
def primer(*args,**kwargs):
gen = func(*args,**kwargs)
next(gen)
return gen
return primer
@coroutine
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total / count
from inspect import getgeneratorstate
coro_avg = averager()
print(getgeneratorstate(coro_avg))
print(coro_avg.send(10))
print(coro_avg.send(30))
輸出:
GEN_SUSPENDED
10.0
20.0
如果使用yield from 語法鬼癣,會自動預激陶贼。asyncio.coroutine裝飾器不會預激活協(xié)程,因此能兼容yield from 句法待秃。
終止協(xié)程和異常處理
協(xié)程中未處理的異常會向上冒泡拜秧,傳給next函數(shù)或send方法的調(diào)用方。
例如:
coro_avg = averager()
print(coro_avg.send(20))
print(coro_avg.send('aa'))
- 由于第三行發(fā)送的不是數(shù)據(jù)章郁,導致協(xié)程內(nèi)部拋出異常枉氮。
- 由于協(xié)程內(nèi)部沒有處理異常志衍,協(xié)程會終止。如果試圖重新激活協(xié)程聊替,會拋出StopIteration異常楼肪。
python 2.5以后,客戶代碼可以調(diào)用以下兩個方法惹悄,顯示地把異常發(fā)給協(xié)程:
- generator.throw(exec_type[,exc_value[,traceback]])
致使生成器在暫停的yield表達式處拋出指定的異常春叫。如果生成器處理了拋出的異常,代碼會向前執(zhí)行到下一個yield表達式泣港,而產(chǎn)出的值會成為調(diào)用generator.throw方法得到的返回值暂殖。如果生成器沒有處理拋出的異常,異常會向上冒泡当纱,傳到調(diào)用方的上下文中呛每。
- generator.close()
致使生成器在暫停的yield表達式處拋出GeneratorExit異常。如果生成器沒有處理這個異常坡氯,或者拋出了StopIteration異常(通常是指運行到結(jié)尾)晨横,調(diào)用方不會報錯。如果收到GeneratorExit異常箫柳,生成器一定不能產(chǎn)出值手形,否則解釋器會拋出RuntimeError異常。生成器拋出的其他異常會向上冒泡滞时,傳給調(diào)用方叁幢。
示例:調(diào)用
class DemoException(Exception):
"""自定義異常"""
def demo_exc_handling():
print("-> oroutine started")
while True:
try :
x = yield
except DemoException:
print('*** DemoException handled. Continuing...')
else :
print('-> coroutine received: {!r}'.format(x))
raise RuntimeError('永遠不執(zhí)行')
exc_coro = demo_exc_handling()
next(exc_coro)
exc_coro.send(11)
exc_coro.send(22)
exc_coro.close()
from inspect import getgeneratorstate
print(getgeneratorstate(exc_coro))
如果傳入 DemoException,則協(xié)程會正常處理,并繼續(xù)運行坪稽。
exc_coro = demo_exc_handling()
next(exc_coro)
exc_coro.send(11)
exc_coro.send(22)
# 將 DemoException 傳入
exc_coro.throw(DemoException)
from inspect import getgeneratorstate
print(getgeneratorstate(exc_coro))
如果傳入的異常沒有處理,協(xié)程會立即停止鳞骤,變成'GEN_CLOSED'狀態(tài)窒百。
exc_coro = demo_exc_handling()
next(exc_coro)
exc_coro.send(11)
exc_coro.send(22)
# 將 DemoException 傳入
exc_coro.throw(ZeroDivisionError)
from inspect import getgeneratorstate
print(getgeneratorstate(exc_coro))
如果協(xié)程必須做一些清理工作,則可以在協(xié)程體中放入 try/finally 代碼塊豫尽。
def demo_finally():
print('-> coroutine started')
try:
while True:
try:
x = yield
except DemoException:
print('*** DemoException handled. Continuing...')
else:
print('-> coroutine received: {!r}'.format(x))
finally:
print("->coroutine ending...")
demo_coro = demo_finally();
next(demo_coro)
demo_coro.send(11)
demo_coro.send(ZeroDivisionError)
讓協(xié)程返回值
協(xié)程可以在執(zhí)行時不產(chǎn)出值篙梢,而是在最后返回一個值(通常是累計值)。
例子:一次返回平均值
from collections import namedtuple
Result = namedtuple('Result','count average')
def averager():
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)
coro_avg = averager();
next(coro_avg)
coro_avg.send(10)
coro_avg.send(30)
coro_avg.send(20)
coro_avg.send(None)
#輸出:
Traceback (most recent call last):
File "C:/Users/42072/PycharmProjects/day01/ready04/cor.py", line 109, in <module>
coro_avg.send(None)
StopIteration: Result(count=3, average=20.0)
發(fā)送None會終止循環(huán)美旧,導致協(xié)程結(jié)束渤滞,返回結(jié)果。一如既往榴嗅,生成器對象會拋出StopIteration異常妄呕。異常對象的value屬性保存著返回的值。
coro_avg = averager();
next(coro_avg)
coro_avg.send(10)
coro_avg.send(30)
coro_avg.send(20)
try:
coro_avg.send(None)
except StopIteration as exc:
result = exc.value
print(result)
yield from結(jié)構(gòu)會在內(nèi)部自動捕獲StopIteration異常嗽测。對yield from結(jié)構(gòu)來說绪励,解釋器不僅會捕獲StopIteration異常肿孵,還會把value屬性的值變成yield from表達式的值。
yield from 句法
在生成器gen中使用yield from subgen()時疏魏,subgen會獲得控制權(quán)停做,把產(chǎn)出的值傳給gen的調(diào)用方,即調(diào)用方可以直接控制subgen大莫。與此同時蛉腌,gen會阻塞,等待subgen終止只厘。
用來簡化for循環(huán)中的yield表達式
def gen():
for c in 'AB':
yield c
for i in range(1,3):
yield i
print(list(gen()))
# 可以改寫為:
def gen():
yield from 'AB'
yield from range(1,3)
print(list(gen()))
yield from x表達式對x對象所做的第一件事是烙丛,調(diào)用iter(x),從中獲取迭代器懈凹。因此蜀变,x可以是任何可迭代的對象。
把職責委托給子生成器
yield from 的主要功能是打開雙向通道介评,把最外層的調(diào)用方與最內(nèi)層的子生成器連接起來库北,這樣二者可以直接發(fā)送和產(chǎn)出值,還可以直接傳入異常们陆,而不用在位于中間的協(xié)程中添加大量處理異常的樣板代碼寒瓦。通過這個結(jié)構(gòu),協(xié)程可以把功能委托給子生成器坪仇。
主要術(shù)語:
委派生成器:
包含 yield from <iterable> 表達式的生成器函數(shù)杂腰。子生成器:
從 yield from 表達式中 <iterable>部分獲取的生成器。調(diào)用方
調(diào)用委派生成器的客戶端代碼椅文。
graph LR
調(diào)用方-->委派生成器
委派生成器-->子生成器
示例:
計算7年級學生的體重和身高的平均值:
from collections import namedtuple
Result = namedtuple('Result','count average')
# 子生成器
def averager():
total = 0.0
count = 0
average = None
while True:
# main 方法中發(fā)送的各種值喂很,會綁定到term變量上
term = yield
# 子生成器終止的條件
if term is None:
break
total += term
count += 1
average = total / count
# 返回值會成為grouper中 yield from表達式的值
return Result(count,average)
# 委派生成器
def grouper(results,key):
while True:
# 每次迭代都會生成一個averager實例。每個生成器都是本協(xié)程(grouper)使用的生成器對象皆刺。
results[key] = yield from averager()
# 客戶端代碼
def main(data):
results = {}
for key,values in data.items():
# results 用來存儲結(jié)果
group = grouper(results, key)
# 預激活協(xié)程
next(group)
for value in values:
# 發(fā)送的每個值都會經(jīng)由grouper的yield from處理少辣,通過管道傳給averager實例。同時羡蛾,當前的grouper實例漓帅,會在yield from 處暫停。
group.send(value)
# 把None值傳入grouper痴怨,導致當前的averager實例終止忙干,并讓grouper繼續(xù)運行,再創(chuàng)建一個aveager實例浪藻,處理下一組值捐迫。
group.send(None)
print(results)
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]
}
main(data)
# 輸出:
{'girls;kg': Result(count=10, average=42.040000000000006), 'girls;m': Result(count=10, average=1.4279999999999997), 'boys;kg': Result(count=9, average=40.422222222222224), 'boys;m': Result(count=9, average=1.3888888888888888)}
委派生成器在yield from 表達式處暫停時,調(diào)用方可以直接把數(shù)據(jù)發(fā)給子生成器珠移,子生成器再把產(chǎn)出的值發(fā)給調(diào)用方弓乙。子生成器返回之后末融,解釋器會拋出StopIteration異常,并把返回值附加到異常對象上暇韧,此時委派生成器會恢復運行勾习。
注意:
- 如果子生成器不終止,委派生成器會在yield from處永遠暫停懈玻。
- 因為委派生成器相當于管道巧婶,所以可以把任意數(shù)量個委派生成器連接在一起:一個委派生成器使用yield from調(diào)用一個子生成器,而那個子生成器本身也是委派生成器涂乌,使用yield from調(diào)用另一個子生成器艺栈,以此類推。最終湾盒,這個鏈條要以一個只使用yield表達式的簡單生成器結(jié)束湿右;不過,也能以任何可迭代的對象結(jié)束罚勾。
- 任何yield from鏈條都必須由客戶驅(qū)動毅人,在最外層委派生成器上調(diào)用next(...)函數(shù)或.send(...)方法。
yield from的意義
- 子生成器產(chǎn)出的值都直接傳給委派生成器的調(diào)用方(即客戶端代碼)尖殃。
- 使用send()方法發(fā)給委派生成器的值都直接傳給子生成器丈莺。如果發(fā)送的值是None,那么會調(diào)用子生成器的next()方法送丰。如果發(fā)送的值不是None缔俄,那么會調(diào)用子生成器的send()方法。如果調(diào)用的方法拋出StopIteration異常器躏,那么委派生成器恢復運行俐载。任何其他異常都會向上冒泡,傳給委派生成器登失。
- 生成器退出時瞎疼,生成器(或子生成器)中的return expr表達式會觸發(fā)StopIteration(expr)異常拋出。
- yield from表達式的值是子生成器終止時傳給StopIteration異常的第一個參數(shù)壁畸。
- 傳入委派生成器的異常,除了GeneratorExit之外都傳給子生成器的throw()方法茅茂。如果調(diào)用throw()方法時拋出StopIteration異常捏萍,委派生成器恢復運行。StopIteration之外的異常會向上冒泡空闲,傳給委派生成器令杈。
- 如果把GeneratorExit異常傳入委派生成器,或者在委派生成器上調(diào)用close()方法碴倾,那么在子生成器上調(diào)用close()方法逗噩,如果它有的話掉丽。如果調(diào)用close()方法導致異常拋出,那么異常會向上冒泡异雁,傳給委派生成器捶障;否則,委派生成器拋出GeneratorExit異常纲刀。
協(xié)程能自然地表述很多算法项炼,例如仿真、游戲示绊、異步I/O锭部,以及其他事件驅(qū)動型編程形式或協(xié)作式多任務。
案例:出租車運營仿真
import collections
# time 事件發(fā)生時間 proc:出租車編號 action:活動描述
Event = collections.namedtuple('Event','time proc action')
# 每輛出租車調(diào)用一次該函數(shù)面褐,用于創(chuàng)建一個生成器對象拌禾,用來表示各輛出租車的運營過程。
# ident是出租車編號
# trips 是出租車回家之前的形成數(shù)量
def taxi_process(ident,trips, start_time = 0):
"""每次改變狀態(tài)時創(chuàng)建事件展哭,把控制權(quán)讓給仿真器"""
# 離開停車場事件湃窍,執(zhí)行到此,會暫停摄杂。當需要重新激活這個進程時坝咐,主循環(huán)會使用send方法發(fā)送當前的仿真事件賦值給time
time = yield Event(start_time,ident,'leave garage')
#每次行程都會執(zhí)行一遍此處的代碼塊
for i in range(trips):
# 產(chǎn)生一個Event實例,表示拉到乘客了析恢。協(xié)程在這里會暫停墨坚,需要激活時,主循環(huán)會使用send方法發(fā)送當前時間映挂。
time = yield Event(time,ident,'pick up passenger')
time = yield Event(time,ident,'drop off passenger')
# 指定行程數(shù)量完成后泽篮,產(chǎn)生回家事件。此處柑船,協(xié)程最后一次暫停帽撑。
yield Event(time,ident,'going home')
taxi = taxi_process(ident=12,trips=2,start_time=0)
next(taxi)
print(taxi.send( 7))
print(taxi.send(10))
print(taxi.send(15))
print(taxi.send(25))
print(taxi.send(35))
# 輸出:
Event(time=7, proc=12, action='pick up passenger')
Event(time=10, proc=12, action='drop off passenger')
Event(time=15, proc=12, action='pick up passenger')
Event(time=25, proc=12, action='drop off passenger')
Event(time=35, proc=12, action='going home')