【節選自《流暢的Python》第16章-協程】python
1、綜述算法
字典爲動詞「to yield」給出了兩個釋義:產出和讓步。對於 Python 生成器中的 yield 來講,這兩個含義都成立。編程
yield item 這行代碼會產出一個值,提供給 next(...) 的調用方;數據結構
此外,還會做出讓步,暫停執行生成器,讓調用方繼續工做,直到須要使用另外一個值時再調用next()。調用方會從生成器中拉取值。多線程
從句法上看,協程與生成器相似,都是定義體中包含 yield 關鍵字的函數。閉包
但是,在協程中,yield 一般出如今表達式的右邊(例如,datum = yield),能夠產出值,也能夠不產出——併發
若是 yield關鍵字後面沒有表達式,那麼生成器產出 None。dom
協程可能會從調用方接收數據,不過調用方把數據提供給協程使用的是 .send(datum) 方法,而不是 next(...) 函數。一般,調用方會把值推送給協程。異步
yield 關鍵字甚至還能夠不接收或傳出數據。async
無論數據如何流動,yield 都是一種流程控制工具,使用它能夠實現協做式多任務:協程能夠把控制器讓步給中心調度程序,從而激活其餘的協程。
從根本上把 yield 視做控制流程的方式,這樣就好理解協程了。
2、協程最簡單的使用演示
def simple_coroutine(): print('-> coroutine started.') x = yield print('-> coroutine received:', x) my_coro = simple_coroutine() print(my_coro) #generator object next(my_coro) print(my_coro.send(42))
結果以下:
一、協程使用生成器函數定義:定義體中有 yield 關鍵字。
二、yield 在表達式中使用;若是協程只需從客戶那裏接收數據,那麼產出的值是 None——這個值是隱式指定的,由於 yield 關鍵字右邊沒有表達式。
三、與建立生成器的方式同樣,調用函數獲得生成器對象。
四、首先要調用 next(...) 函數,由於生成器還沒啓動,沒在 yield 語句處暫停,因此一開始沒法發送數據。
五、調用這個方法後,協程定義體中的 yield 表達式會計算出 42;如今,協程會恢復,一直運行到下一個 yield 表達式,或者終止。
六、最後,控制權流動到協程定義體的末尾,致使生成器像往常同樣拋出 StopIteration 異常。
3、協程能夠身處四個狀態
當前狀態可使用inspect.getgeneratorstate(...) 函數肯定,該函數會返回下述字符串中的一個。
一、'GEN_CREATED' 等待開始執行。
2、'GEN_RUNNING' 解釋器正在執行。
注意:只有在多線程應用中才能看到這個狀態。此外,生成器對象在本身身上調用getgeneratorstate 函數也行,不過這樣作沒什麼用。
三、'GEN_SUSPENDED' 在 yield 表達式處暫停。
4、'GEN_CLOSED' 執行結束。
由於 send 方法的參數會成爲暫停的 yield 表達式的值,因此,僅當協程處於暫停狀態時才能調用 send 方法,例如 my_coro.send(42)。
不過,若是協程還沒激活(即,狀態是 'GEN_CREATED'),狀況就不一樣了。
所以,始終要調用 next(my_coro) 激活協程——也能夠調用my_coro.send(None),效果同樣。
若是建立協程對象後當即把 None 以外的值發給它,會出現下述錯誤:
my_coro = simple_coroutine()
my_coro.send(23)
注意錯誤消息,它表述得至關清楚。最早調用 next(my_coro) 函數這一步一般稱爲「預激」(prime)協程。
(即,讓協程向前執行到第一個 yield 表達式,準備好做爲活躍的協程使用)
下面舉個產出多個值的例子,以便更好地理解協程的行爲:
from inspect import getgeneratorstate def simple_coro2(a): print('-> Started: a=', a) b = yield a print('-> Received: b=', b) c = yield a + b print('-> Received: c=', c) my_coro2 = simple_coro2(14) state = getgeneratorstate(my_coro2) print(state) # GEN_CREATED next(my_coro2) # -> Started: a= 14 state = getgeneratorstate(my_coro2) print(state) # GET_SUSPENDED my_coro2.send(28) # -> Received: b= 28 my_coro2.send(99) # -> Received: c= 99 # Traceback (most recent call last): # File "<stdin>", line 1, in <module> # StopIteration state = getgeneratorstate(my_coro2) print(state) # GEN_CLOSED
一、首先inspect.getgeneratorstate 函數指明,處於 GEN_CREATED 狀態(即協程未啓動)。
二、向前執行協程到第一個 yield 表達式,打印 -> Started: a = 14消息,而後產出(yield)a 的值,而且暫停,等待爲 b 賦值。
三、getgeneratorstate 函數指明,處於 GEN_SUSPENDED 狀態(即協程在 yield 表達式處暫停)。
四、把數字 28 發給暫停的協程;計算 yield 表達式,獲得 28,而後把那個數綁定給 b,打印 -> Received: b = 28 消息,
而後產出(yield) a + b 的值(42),而且協程暫停,等待爲 c 賦值。
五、把數字 99 發給暫停的協程;計算 yield 表達式,獲得 99,而後把那個數綁定給 c,打印 -> Received: c = 99 消息,
而後協程終止,致使生成器對象拋出 StopIteration 異常。
六、最後getgeneratorstate 函數指明,處於 GEN_CLOSED 狀態(即協程執行結束)。
關鍵的一點是,協程在 yield 關鍵字所在的位置暫停執行。前面說過,在賦值語句中,= 右邊的代碼在賦值以前執行。
所以,對於 b =yield a 這行代碼來講,等到客戶端代碼再激活協程時纔會設定 b 的值。
這種行爲要花點時間才能習慣,不過必定要理解,這樣才能弄懂異步編程中 yield 的做用(後文探討)。
simple_coro2 協程的執行過程分爲 3 個階段,以下圖所示。
一、調用 next(my_coro2),打印第一個消息,而後執行 yield a,產出數字 14。
二、調用 my_coro2.send(28),把 28 賦值給 b,打印第二個消息,而後執行 yield a + b,產出數字 42。
三、調用 my_coro2.send(99),把 99 賦值給 c,打印第三個消息,協程終止。
注意,各個階段都在yield 表達式中結束,並且下一個階段都從那一行代碼開始,而後再把 yield 表達式的值賦給變量!
4、示例:使用協程計算平均值
def averager(): total = 0.0 count = 0 average = None while True: term = yield average total += term count += 1 average = total / count
一、這個無限循環代表,只要調用方不斷把值發給這個協程,它就會一直接收值,而後生成結果。
僅當調用方在協程上調用 .close() 方法,或者沒有對協程的引用而被垃圾回收程序回收時,這個協程纔會終止。
二、這裏的 yield 表達式用於暫停執行協程,把結果發給調用方;還用於接收調用方後面發給協程的值,恢復無限循環。
使用協程的好處是,total 和 count 聲明爲局部變量便可,無需使用實例屬性或閉包在屢次調用之間保持上下文。
使用averager協程:
coro_avg = averager() next(coro_avg) avg = coro_avg.send(10) print(avg) #10.0 avg = coro_avg.send(30) print(avg) #20.0 avg = coro_avg.send(5) print(avg) #15.0
一、建立協程對象。
二、調用 next 函數,預激協程。
三、計算平均值:屢次調用 .send(...) 方法,產出當前的平均值。
在上述示例中,調用 next(coro_avg) 函數後,協程會向前執行到 yield 表達式,產出 average 變量的初始值——None,所以不會出如今控制檯中。
此時,協程在 yield 表達式處暫停,等到調用方發送值。
coro_avg.send(10) 那一行發送一個值,激活協程,把發送的值賦給 term,並更新 total、count 和 average 三個變量的值,
而後開始 while 循環的下一次迭代,產出 average 變量的值,等待下一次爲 term 變量賦值。
5、預激協程的裝飾器
若是不預激,那麼協程沒什麼用。調用 my_coro.send(x) 以前,記住必定要調用 next(my_coro)。
爲了簡化協程的用法,有時會使用一個預激裝飾器。
示例:預激協程的裝飾器
from functools import wraps def coroutine(func): '''裝飾器:向前執行到第一個`yield`表達式,預激`func`''' @wraps(func) def primer(*args, **kwargs): #1 gen = func(*args, **kwargs) #2 next(gen) #3 return gen #4 return primer
一、簡單說下裝飾器,假若有個名爲 decorate 的裝飾器:
@decorate def target(): print('running target()')
上述代碼的效果與下述寫法同樣:
def target(): print('running target()')
target = decorate(target)
因此示例中把被裝飾的生成器函數替換成這裏的 primer 函數;調用 primer 函數時,返回預激後的生成器。
二、調用被裝飾的函數,獲取生成器對象。
三、預激生成器。
四、返回生成器。
下面展現 @coroutine 裝飾器的用法:
""" 用於計算移動平均值的協程 >>> coro_avg = averager() #1 >>> from inspect import getgeneratorstate >>> getgeneratorstate(coro_avg) #2 'GEN_SUSPENDED' >>> coro_avg.send(10) #3 10.0 >>> coro_avg.send(30) 20.0 >>> coro_avg.send(5) 15.0 """
from coroutil import coroutine #4 @coroutine #5 def averager(): #6 total = 0.0 count = 0 average = None while True: term = yield average total += term
一、調用 averager() 函數建立一個生成器對象,在 coroutine 裝飾器的 primer 函數中已經預激了這個生成器。
二、getgeneratorstate 函數指明,處於 GEN_SUSPENDED 狀態,所以這個協程已經準備好,能夠接收值了。
三、能夠當即開始把值發給 coro_avg——這正是 coroutine 裝飾器的目的。
四、導入 coroutine 裝飾器。
五、把裝飾器應用到 averager 函數上。
六、函數的定義體
使用 yield from 句法(參見 16.7 節)調用協程時,會自動預激,所以與示例中的 @coroutine 等裝飾器不兼容。
Python 3.4 標準庫裏的 asyncio.coroutine 裝飾器不會預激協程,所以能兼容 yield from 句法。
6、終止協程和異常處理
協程中未處理的異常會向上冒泡,傳給 next 函數或 send 方法的調用方(即觸發協程的對象)。
示例:未處理的異常會致使協程終止
>>> from coroaverager1 import averager >>> coro_avg = averager() >>> coro_avg.send(40) # ➊ 40.0 >>> coro_avg.send(50) 45.0 >>> coro_avg.send('spam') # ➋ Traceback (most recent call last): ... TypeError: unsupported operand type(s) for +=: 'float' and 'str' >>> coro_avg.send(60) # ➌ Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
一、使用 @coroutine 裝飾器裝飾的 averager 協程,能夠當即開始發送值。
二、發送的值不是數字,致使協程內部有異常拋出。
三、因爲在協程內沒有處理異常,協程會終止。若是試圖從新激活協程,會拋出 StopIteration 異常。
出錯的緣由是,發送給協程的 'spam' 值不能加到 total 變量上。
示例暗示了終止協程的一種方式:發送某個哨符值,讓協程退出。
從 Python 2.5 開始,客戶代碼能夠在生成器對象上調用兩個方法,顯式地把異常發給協程。
這兩個方法是 throw 和 close。
generator.throw(exc_type[, exc_value[, traceback]])
導致生成器在暫停的 yield 表達式處拋出指定的異常。若是生成器處理了拋出的異常,代碼會向前執行到下一個 yield 表達式,而產出的值會成爲調用generator.throw 方法獲得的返回值。若是生成器沒有處理拋出的異常,異常會向上冒泡,傳到調用方的上下文中。
generator.close()
導致生成器在暫停的 yield 表達式處拋出 GeneratorExit 異常。若是生成器沒有處理這個異常,或者拋出了 StopIteration 異常(一般是指運行到結尾),調用方不會報錯。若是收到 GeneratorExit 異常,生成器必定不能產出值,不然解釋器會拋出 RuntimeError 異常。生成器拋出的其餘異常會向上冒泡,傳給調用方。
下面舉例說明如何使用 close 和 throw 方法控制協程。
示例:學習在協程中處理異常的測試代碼
class DemoException(Exception): """爲此次演示定義的異常類型""" def demo_exc_handling(): print('-> coroutine started.') while True: try: x = yield except DemoException: #1 print('*** DemoException handled. Continuing...') else: #2 print('-> coroutine received: {!r}'.format(x)) raise RuntimeError('This line should never run.') #3
一、特別處理 DemoException 異常。
二、若是沒有異常,那麼顯示接收到的值。
三、這一行永遠不會執行,由於只有未處理的異常纔會停止那個無限循環,而一旦出現未處理的異常,協程會當即終止。
實驗1 :激活和關閉 demo_exc_handling,沒有異常
exc_coro = demo_exc_handling() next(exc_coro) # -> coroutine started. exc_coro.send(11) # -> coroutine received: 11 exc_coro.send(22) # -> coroutine received: 22 exc_coro.close() from inspect import getgeneratorstate print(getgeneratorstate(exc_coro)) # GEN_CLOSED
若是把 DemoException 異常傳入 demo_exc_handling 協程,它會處理,而後繼續運行,以下面的 示例2 所示。
實驗2 :把 DemoException 異常傳入 demo_exc_handling 不會致使協程停止
exc_coro = demo_exc_handling() next(exc_coro) # -> coroutine started. exc_coro.send(11) # -> coroutine received: 11 exc_coro.throw(DemoException) # *** DemoException handled. Continuing... from inspect import getgeneratorstate print(getgeneratorstate(exc_coro)) # GEN_SUSPENDED
可是,若是傳入協程的異常沒有處理,協程會中止,即狀態變成'GEN_CLOSED'。
實驗3 :若是沒法處理傳入的異常,協程會終止
exc_coro = demo_exc_handling() next(exc_coro) # -> coroutine started. exc_coro.send(11) # -> coroutine received: 11 exc_coro.throw(ZeroDivisionError) # Traceback (most recent call last)...ZeroDivisionError from inspect import getgeneratorstate print(getgeneratorstate(exc_coro)) # 因爲上面異常致使程序終止,但狀態是GEN_CLOSED
若是無論協程如何結束都想作些清理工做,要把協程定義體中相關的代碼放入 try/finally 塊中。
實驗4:使用 try/finally 塊在協程終止時執行操做
def demo_exc_handling(): 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')
Python 3.3 引入 yield from 結構的主要緣由之一與把異常傳入嵌套的協程有關。另外一個緣由是讓協程更方便地返回值。
7、讓協程返回值
下面的示例是 averager 協程的不一樣版本,這一版會返回結果。爲了說明如何返回值,每次激活協程時不會產出移動平均值。
這麼作是爲了強調某些協程不會產出值,而是在最後返回一個值(一般是某種累計值)。
示例中的 averager 協程返回的結果是一個 namedtuple,兩個字段分別是項數(count)和平均值(average)。
我本能夠只返回平均值,可是返回一個元組能夠得到累積數據的另外一個重要信息——項數。
示例:定義一個求平均值的協程,讓它返回一個結果
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 #1 total += term count += 1 average = total / count return Result(count, average) #2
一、爲了返回值,協程必須正常終止;所以,這一版 averager 中有個條件判斷,以便退出累計循環。
二、返回一個 namedtuple,包含 count 和 average 兩個字段。在 Python 3.3 以前,若是生成器返回值,解釋器會報句法錯誤。
示例:如何使用新版averager ()
coro_avg = averager() next(coro_avg) coro_avg.send(10) #1 coro_avg.send(30) coro_avg.send(6.5) coro_avg.send(None) #2
Traceback (most recent call last):
File "...", line 24, in <module>
coro_avg.send(None)
StopIteration: Result(count=3, average=15.5)
一、這一版不產出值。
二、發送 None 會終止循環,致使協程結束,返回結果。一如既往,生成器對象會拋出StopIteration 異常。
異常對象的 value 屬性保存着返回的值。
注意:return 表達式的值會偷偷傳給調用方,賦值給 StopIteration 異常的一個屬性。
這樣作有點不合常理,可是能保留生成器對象的常規行爲——耗盡時拋出StopIteration 異常。
示例:捕獲 StopIteration 異常,獲取 averager 返回的值
coro_avg = averager() next(coro_avg) coro_avg.send(10) #1 coro_avg.send(30) coro_avg.send(6.5) try: coro_avg.send(None) except StopIteration as exc: result = exc.value print(result)
獲取協程的返回值雖然要繞個圈子,但這是 PEP 380 定義的方式,當咱們意識到這一點以後就說得通了:yield from 結構會在內部自動捕獲 StopIteration 異常。
這種處理方式與 for 循環處理 StopIteration 異常的方式同樣:循環機制使用用戶易於理解的方式處理異常。
對 yield from 結構來講,解釋器不只會捕獲 StopIteration 異常,還會把value 屬性的值變成 yield from 表達式的值(=號 左邊的)。
惋惜,咱們沒法在控制檯中使用交互的方式測試這種行爲,由於在函數外部使用 yield from(以及 yield)會致使句法出錯。
8、使用yield from
首先要知道,yield from 是全新的語言結構。它的做用比 yield 多不少,所以人們認爲繼續使用那個關鍵字多少會引發誤解。
在其餘語言中,相似的結構使用 await 關鍵字,這個名稱好多了,由於它傳達了相當重要的一點:
在生成器 gen 中使用 yield from subgen() 時,subgen 會得到控制權,把產出的值傳給 gen 的調用方,即調用方能夠直接控制 subgen。與此同時,gen 會阻塞,等待 subgen 終止。
該書前面內容說過,yield from 可用於簡化 for 循環中的 yield 表達式。例如:
def gen(): for c in 'AB': yield c for i in range(1, 3): yield i li = [n for n in gen()] print(li)
輸出: ['A', 'B', 1, 2]
能夠改寫爲:
def gen(): yield from 'AB' yield from range(1, 3) li = [n for n in gen()] print(li)
輸出: ['A', 'B', 1, 2]
示例:使用 yield from 連接可迭代的對象
def chain(*iterables): for it in iterables: yield from it s = 'ABC' t = tuple(range(3)) li = list(chain(s, t)) print(li) # ['A', 'B', 'C', 0, 1, 2]
注意: s(字符串)和t(元祖)都是可迭代的對象,生成器也是可迭代的對象!
在 Beazley 與 Jones 的《Python Cookbook(第 3 版)中文版》一書中,「4.14 扁平化處理套型的序列」一節有個稍微複雜(不過更有用)的 yield from 示例:
# Example of flattening a nested sequence using subgenerators from collections import Iterable def flatten(items, ignore_types=(str, bytes)): for x in items: if isinstance(x, Iterable) and not isinstance(x, ignore_types): yield from flatten(x) else: yield x items = [1, 2, [3, 4, [5, 6], 7], 8] # Produces 1 2 3 4 5 6 7 8 for x in flatten(items): print(x) items = ['Dave', 'Paula', ['Thomas', 'Lewis']] for x in flatten(items): print(x)
yield from x 表達式對 x 對象所作的第一件事是,調用 iter(x),從中獲取迭代器。所以,x 能夠是任何可迭代的對象。
但是,若是 yield from 結構惟一的做用是替代產出值的嵌套 for 循環,這個結構頗有可能不會添加到 Python 語言中。
yield from 結構的本質做用沒法經過簡單的可迭代對象說明,而要發散思惟,使用嵌套的生成器。
所以,引入 yield from 結構的 PEP 380 才起了「Syntax for Delegating to a Subgenerator」(「把職責委託給子生成器的句法」)這個標題。
yield from 的主要功能是打開雙向通道,把最外層的調用方與最內層的子生成器鏈接起來,這樣兩者能夠直接發送和產出值,還能夠直接傳入異常,而不用在位於中間的協程中添加大量處理異常的樣板代碼。有了這個結構,協程能夠經過之前不可能的方式委託職責。
若想使用 yield from 結構,就要大幅改動代碼。爲了說明須要改動的部分,PEP 380 使用了一些專門的術語。
一、委派生成器: 包含 yield from <iterable> 表達式的生成器函數。
二、子生成器: 從 yield from 表達式中 <iterable> 部分獲取的生成器。
這就是 PEP 380 的標題(「Syntax for Delegating to a Subgenerator」)中所說的「子生成器」(subgenerator)。
三、調用方:PEP 380 使用「調用方」這個術語指代調用委派生成器的客戶端代碼。在不一樣的語境中,我會使用「客戶端」代替「調用方」,以此與委派生成器(也是調用方,由於它調用了子生成器)區分開。
示例:使用 yield from 計算平均值並輸出統計報告
# BEGIN YIELD_FROM_AVERAGER from collections import namedtuple Result = namedtuple('Result', 'count average') # the subgenerator def averager(): # <1> total = 0.0 count = 0 average = None while True: term = yield # <2> if term is None: # <3> break total += term count += 1 average = total/count return Result(count, average) # <4> # the delegating generator def grouper(results, key): # <5> while True: # <6> results[key] = yield from averager() # <7> # the client code, a.k.a. the caller def main(data): # <8> results = {} for key, values in data.items(): group = grouper(results, key) # <9> next(group) # <10> for value in values: group.send(value) # <11> group.send(None) # important! <12> # print(results) # uncomment to debug report(results) # output report def report(results): for key, result in sorted(results.items()): group, unit = key.split(';') print('{:2} {:5} averaging {:.2f}{}'.format( result.count, group, result.average, 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) # END YIELD_FROM_AVERAGER
程序輸出:
9 boys averaging 40.42kg 9 boys averaging 1.39m 10 girls averaging 42.04kg 10 girls averaging 1.43m
一、做爲子生成器使用;
二、main函數中客戶代碼發送的各個值綁定到這裏的term變量上;
三、相當重要的終止條件,若是不這麼作,使用yield from調用這個協程的生成器會永遠阻塞;
四、返回的Result會成爲grouper函數中yield from表達式的值(=號左邊的);
五、grouper是委派生成器;
六、這個循環每次迭代時會新建一個averager實例;每一個實例都是做爲協程使用的生成器對象;
七、grouper發送的每一個值都會經由yield from處理,經過管道傳給averager實例。averager實例運行完畢後,返回的值綁定到result[key]上。
while循環會不斷建立averager實例,處理更多的值。
八、main函數是客戶端代碼,用PE380的術語來講,是「調用方」,是驅動一切的函數。
九、group 是調用 grouper 函數獲得的生成器對象,傳給 grouper 函數的第一個參數是results,用於收集結果;第二個參數是某個鍵。group 做爲協程使用。
十、預激 group 協程。
十一、把各個 value 傳給 grouper。傳入的值最終到達 averager 函數中 term = yield 那一行;grouper 永遠不知道傳入的值是什麼。
十二、把 None 傳入 grouper,致使當前的 averager 實例終止,也讓 grouper 繼續運行,再建立一個 averager 實例,處理下一組值。
註釋——「重要!」,強調這行代碼(group.send(None))相當重要:終止當前的 averager 實例,開始執行下一個。
若是註釋掉那一行,這個腳本不會輸出任何報告。此時,把 main 函數靠近末尾的print(results) 那行的註釋去掉,你會發現,results 字典是空的。
下圖將示例中各個相關的部分標識出來了:
委派生成器在 yield from 表達式處暫停時,調用方能夠直接把數據發給子生成器,子生成器再把產出的值發給調用方。
子生成器返回以後,解釋器會拋出StopIteration 異常,並把返回值附加到異常對象上,此時委派生成器會恢復
下面簡要說明示例的運做方式,還會說明把 main 函數中調用 group.send(None)那一行代碼(帶有「重要!」註釋的那一行)去掉會發生什麼事。
一、外層 for 循環每次迭代會新建一個 grouper 實例,賦值給 group 變量;group 是委派生成器。
二、調用 next(group),預激委派生成器 grouper,此時進入 while True 循環,調用子生成器 averager 後,在 yield from 表達式處暫停。
三、內層 for 循環調用 group.send(value),直接把值傳給子生成器 averager。同時,當前的 grouper 實例(group)在 yield from 表達式處暫停。
四、內層循環結束後,group 實例依舊在 yield from 表達式處暫停,所以,grouper函數定義體中爲 results[key] 賦值的語句尚未執行。
五、若是外層 for 循環的末尾沒有 group.send(None),那麼 averager 子生成器永遠不會終止,委派生成器 group 永遠不會再次激活,所以永遠不會爲 results[key]賦值。
六、外層 for 循環從新迭代時會新建一個 grouper 實例,而後綁定到 group 變量上。前一個 grouper 實例(以及它建立的還沒有終止的 averager 子生成器實例)被垃圾回收程序回收。
這個試驗想代表的關鍵一點是,若是子生成器不終止,委派生成器會在yield from 表達式處永遠暫停。若是是這樣,程序不會向前執行,由於 yield from(與 yield 同樣)把控制權轉交給客戶代碼(即,委派生成器的調用方)了。顯然,確定有任務沒法完成。
示例展現了 yield from 結構最簡單的用法,只有一個委派生成器和一個子生成器。由於委派生成器至關於管道,因此能夠把任意數量個委派生成器鏈接在一塊兒:一個委派生成器使用 yield from 調用一個子生成器,而那個子生成器自己也是委派生成器,使用 yield from 調用另外一個子生成器,以此類推。最終,這個鏈條要以一個只使用 yield表達式的簡單生成器結束;不過,也能以任何可迭代的對象結束,如示例所示。
任何 yield from 鏈條都必須由客戶驅動,在最外層委派生成器上調用 next(...) 函數或 .send(...) 方法。能夠隱式調用,例如使用 for 循環。
下面綜述 PEP 380 對 yield from 結構的正式說明。
9、yield from的意義
PEP380 草稿中有這樣一段話:「把迭代器看成生成器使用,至關於把子生成器的定義體內聯在 yield from 表達式中。此外,子生成器能夠執行 return 語句,返回一個值,而返回的值會成爲 yield from 表達式的值。」
PEP 380 中已經沒有這段寬慰人心的話,由於沒有涵蓋全部極端狀況。
批准後的 PEP 380 在「Proposal」一節(https://www.python.org/dev/peps/pep-0380/#proposal)分六點說明了 yield from 的行爲。這裏,我幾乎原封不動地引述,不過把有歧義的「迭代器」一詞都換成了「子生成器」,還作了進一步說明。上一節的示例闡明瞭下述四點。
一、子生成器產出的值都直接傳給委派生成器的調用方(即客戶端代碼)。
二、使用 send() 方法發給委派生成器的值都直接傳給子生成器。若是發送的值是None,那麼會調用子生成器的 __next__() 方法。若是發送的值不是 None,那麼會調用子生成器的 send() 方法。若是調用的方法拋出 StopIteration 異常,那麼委派生成器恢復運行。任何其餘異常都會向上冒泡,傳給委派生成器。
三、生成器退出時,生成器(或子生成器)中的 return expr 表達式會觸發StopIteration(expr) 異常拋出。
四、yield from 表達式的值是子生成器終止時傳給 StopIteration 異常的第一個參數。
yield from的另外兩個特性與異常和終止有關:
一、傳入委派生成器的異常,除了 GeneratorExit 以外都傳給子生成器的 throw() 方法。若是調用 throw() 方法時拋出 StopIteration 異常,委派生成器恢復運行。StopIteration 以外的異常會向上冒泡,傳給委派生成器。
二、若是把 GeneratorExit 異常傳入委派生成器,或者在委派生成器上調用 close() 方法,那麼在子生成器上調用 close() 方法,若是它有的話。若是調用 close() 方法致使異常拋出,那麼異常會向上冒泡,傳給委派生成器;不然,委派生成器拋出GeneratorExit 異常。
yield from 的具體語義很難理解,尤爲是處理異常的那兩點。若想仔細研究,最好將其簡化,只涵蓋 yield from 最基本且最多見的用法。
假設 yield from 出如今委派生成器中。客戶端代碼驅動着委派生成器,而委派生成器驅動着子生成器。那麼,爲了簡化涉及到的邏輯,咱們假設客戶端沒有在委派生成器上調用.throw(...) 或.close() 方法。此外,咱們還假設子生成器不會拋出異常,而是一直運行到終止,讓解釋器拋出 StopIteration 異常。下面來看一下在這個簡化的美滿世界中,yield from 是如何運做的。
請看示例,那裏列出的代碼是委派生成器的定義體中下面這一行代碼的擴充:
RESULT = yield from EXPR
示例:簡化的僞代碼,等效於委派生成器中的 RESULT = yield from EXPR語句(這裏針對的是最簡單的狀況:不支持 .throw(...) 和 .close() 方法,並且只處理 StopIteration 異常)
_i = iter(EXPR) #1 try: _y = next(_i) #2 except StopIteration as _e: _r = _e.value #3 else: while 1: #4 _s = yield _y #5 try: _y = _i.send(_s) #6 except StopIteration as _e: #7 _r = _e.value break RESULT = _r
一、EXPR能夠是任何可迭代的對象,由於獲取迭代器_i(這是子生成器)使用的是iter()函數;
二、預激子生成器,結果保存在_y中,做爲產出的第一個值;
三、若是拋出StopIteration異常,獲取異常對象的value屬性,賦值給_r——這是最簡單狀況下的返回值(RESULT)
四、運行這個循環時,委派生成器會阻塞,只做爲調用方和子生成器之間的通道;
五、產出子生成器當前產出的元素;等待調用方發送_s中保存的值。注意,這個代碼清單中只有這一個yield表達式;
六、嘗試讓子生成器向前執行,轉發調用方發送的_s;
七、若是子生成器拋出StopIteration異常,獲取value屬性的值,賦值給_r,而後退出循環,讓委派生成器恢復運行;
八、返回的結果是(RESULT)是_r,即整個yield from表達式的值。
在這段簡化的僞代碼中,我保留了 PEP 380 中那段僞代碼使用的變量名稱。這些變量是:
_i(迭代器)
子生成器
_y(產出的值)
子生成器產出的值
_r(結果)
最終的結果(即子生成器運行結束後 yield from 表達式的值)
_s(發送的值)
調用方發給委派生成器的值,這個值會轉發給子生成器
_e(異常)
異常對象(在這段簡化的僞代碼中始終是 StopIteration 實例)
除了沒有處理 .throw(...) 和 .close() 方法以外,這段簡化的僞代碼還在子生成器上調用 .send(...) 方法,以此達到客戶調用next() 函數或 .send(...) 方法的目的。首次閱讀時不要擔憂這些細微的差異。前面說過,即便 yield from 結構只作上一節示例展現的事情,也依舊能正常運行。
可是,現實狀況要複雜一些,由於要處理客戶對 .throw(...) 和 .close() 方法的調用,而這兩個方法執行的操做必須傳入子生成器。此外,子生成器可能只是純粹的迭代器,不支持 .throw(...) 和 .close() 方法,所以 yield from 結構的邏輯必須處理這種狀況。若是子生成器實現了這兩個方法,而在子生成器內部,這兩個方法都會觸發異常拋出,這種狀況也必須由 yield from 機制處理。調用方可能會平白無故地讓子生成器本身拋出異常,實現 yield from 結構時也必須處理這種狀況。最後,爲了優化,若是調用方調用 next(...) 函數或 .send(None) 方法,都要轉交職責,在子生成器上調用next(...) 函數;僅當調用方發送的值不是 None 時,才使用子生成器的 .send(...) 方法。
爲了方便對比,下面列出 PEP 380 中擴充 yield from 表達式的完整僞代碼,並且加上了帶標號的註解。下面示例中的代碼是一字不差複製過來的,只有標註是我本身加的。
再次說明,示例中的代碼是委派生成器的定義體中下面這一個語句的擴充:
RESULT = yield from EXPR
示例 :僞代碼,等效於委派生成器中的 RESULT = yield from EXPR 語句
_i = iter(EXPR) #1 try: _y = next(_i) #2 except StopIteration as _e: _r = _e.value #3 else: while 1: #4 try: _s = yield _y #5 except GeneratorExit as _e: #6 try: _m = _i.close except AttributeError: pass else: _m() raise _e except BaseException as _e: #7 _x = sys.exc_info() try: _m = _i.throw except AttributeError: raise _e else: #8 try: _y = _m(*_x) except StopIteration as _e: _r = _e.value break else: #9 try: #10 if _s is None: #11 _y = next(_i) else: _y = _i.send(_s) except StopIteration as _e: #12 _r = _e.value break RESULT = _r #13
一、 EXPR 能夠是任何可迭代的對象,由於獲取迭代器 _i(這是子生成器)使用的是iter() 函數。
二、預激子生成器;結果保存在 _y 中,做爲產出的第一個值。
三、若是拋出 StopIteration 異常,獲取異常對象的 value 屬性,賦值給 _r——這是最簡單狀況下的返回值(RESULT)。
四、運行這個循環時,委派生成器會阻塞,只做爲調用方和子生成器之間的通道。
五、產出子生成器當前產出的元素;等待調用方發送 _s 中保存的值。這個代碼清單中只有這一個 yield 表達式。
六、這一部分用於關閉委派生成器和子生成器。由於子生成器能夠是任何可迭代的對象,因此可能沒有 close 方法。
七、這一部分處理調用方經過 .throw(...) 方法傳入的異常。一樣,子生成器能夠是迭代器,從而沒有 throw 方法可調用——這種狀況會致使委派生成器拋出異常。
八、若是子生成器有 throw 方法,調用它並傳入調用方發來的異常。子生成器可能會處理傳入的異常(而後繼續循環);可能拋出 StopIteration 異常(從中獲取結果,賦值給_r,循環結束);還可能不處理,而是拋出相同的或不一樣的異常,向上冒泡,傳給委派生成器。
九、若是產出值時沒有異常……
十、嘗試讓子生成器向前執行……
十一、若是調用方最後發送的值是 None,在子生成器上調用 next 函數,不然調用 send 方法。
十二、若是子生成器拋出 StopIteration 異常,獲取 value 屬性的值,賦值給 _r,而後退出循環,讓委派生成器恢復運行。
1三、返回的結果(RESULT)是 _r,即整個 yield from 表達式的值。
這段 yield from 僞代碼的大多數邏輯經過六個 try/except 塊實現,並且嵌套了四層,所以有點難以閱讀。此外,用到的其餘流程控制關鍵字有一個 while、一個 if 和一個yield。找到 while 循環、yield 表達式以及 next(...) 函數和 .send(...) 方法調用,這些代碼有助於對 yield from 結構的運做方式有個總體的瞭解。
就在示例所列僞代碼的頂部,有行代碼(標號❷)揭示了一個重要的細節:要預激子生成器。 這代表,用於自動預激的裝飾器與 yield from 結構不兼容。
仔細研究擴充的僞代碼可能沒什麼用——這與你的學習方式有關。顯然,分析真正使用yield from 結構的代碼要比深刻研究實現這一結構的僞代碼更有好處。不過,我見過的yield from 示例幾乎都使用 asyncio 模塊作異步編程,所以要有有效的事件循環才能運行。
下面分析一個使用協程的經典案例:仿真編程。這個案例沒有展現 yield from 結構的用法,可是揭示瞭如何使用協程在單個線程中管理併發活動。
10、使用案例:使用協程作離散事件仿真
協程是 asyncio 包的基礎構建。經過仿真系統能說明如何使用協程代替線程實現併發的活動,並且對理解asyncio 包有極大的幫助。
一、離散事件仿真簡介
離散事件仿真(Discrete Event Simulation,DES)是一種把系統建模成一系列事件的仿真類型。在離散事件仿真中,仿真「鍾」向前推動的量不是固定的,而是直接推動到下一個事件模型的模擬時間。假如咱們抽象模擬出租車的運營過程,其中一個事件是乘客上車,下一個事件則是乘客下車。無論乘客坐了 5 分鐘仍是 50 分鐘,一旦乘客下車,仿真鍾就會更新,指向這次運營的結束時間。使用離散事件仿真能夠在不到一秒鐘的時間內模擬一年的出租車運營過程。這與連續仿真不一樣,連續仿真的仿真鍾以固定的量(一般很小)不斷向前推動。
顯然,回合制遊戲就是離散事件仿真的例子:遊戲的狀態只在玩家操做時變化,並且一旦玩家決定下一步怎麼走了,仿真鍾就會凍結。而實時遊戲則是連續仿真,仿真鍾一直在運行,遊戲的狀態在一秒鐘以內更新不少次,所以反應慢的玩家特別吃虧。這兩種仿真類型都能使用多線程或在單個線程中使用面向事件的編程技術(例如事件循環驅動的回調或協程)實現。能夠說,爲了實現連續仿真,在多個線程中處理實時並行的操做更天然。而協程剛好爲實現離散事件仿真提供了合理的抽象。
在仿真領域,進程這個術語指代模型中某個實體的活動,與操做系統中的進程無關。仿真系統中的一個進程可使用操做系統中的一個進程實現,可是一般會使用一個線程或一個協程實現。
二、出租車隊運營仿真
仿真程序 taxi_sim.py 會建立幾輛出租車,每輛車會拉幾個乘客,而後回家。出租車首先駛離車庫,四處徘徊,尋找乘客;拉到乘客後,行程開始;乘客下車後,繼續四處徘徊。四處徘徊和行程所用的時間使用指數分佈生成。爲了讓顯示的信息更加整潔,時間使用取整的分鐘數,不過這個仿真程序也能使用浮點數表示耗時。 每輛出租車每次的狀態變化都是一個事件。下圖 是運行這個程序的輸出示例。
圖 1:運行 taxi_sim.py 建立 3 輛出租車的輸出示例。-s 3 參數設置隨機數生成器的種子,這樣在調試和演示時能夠重複運行程序,輸出相同的結果。不一樣顏色的箭頭表示不一樣出租車的行程.
圖中最值得注意的一件事是,3 輛出租車的行程是交叉進行的。那些箭頭是我加上的,爲的是讓你看清各輛出租車的行程:箭頭從乘客上車時開始,到乘客下車後結束。有了箭頭,能直觀地看出如何使用協程管理併發的活動。
圖中還有幾件事值得注意。
一、出租車每隔 5 分鐘從車庫中出發。
二、0 號出租車 2 分鐘後拉到乘客(time=2),1 號出租車 3 分鐘後拉到乘客(time=8),2 號出租車 5 分鐘後拉到乘客(time=15)。
三、0 號出租車拉了兩個乘客(紫色箭頭):第一個乘客從 time=2 時上車,到 time=18時下車;第二個乘客從 time=28 時上車,到 time=65 時下車——這是這次仿真中最長的行程。
四、1 號出租車拉了四個乘客(綠色箭頭),在 time=110 時回家。
五、2 號出租車拉了六個乘客(紅色箭頭),在 time=109 時回家。這輛車最後一次行程從 time=97 時開始,只持續了一分鐘。
六、1 號出租車的第一次行程從 time=8 時開始,在這個過程當中 2 號出租車離開了車庫(time=10),並且完成了兩次行程(那兩個短的紅色箭頭)。
七、在這次運行示例中,全部排定的事件都在默認的仿真時間內(180 分鐘)完成;最後一次事件發生在 time=110 時。
本章只會列出taxi_sim.py中與協程相關的部分。真正重要的函數只有兩個:taxi_process(一個協程),以及執行仿真主循環的 Simulator.run方法。
示例 16-20 是 taxi_process 函數的代碼。這個協程用到了別處定義的兩個對象:compute_delay 函數,返回單位爲分鐘的時間間隔;Event 類,一個namedtuple,定義方式以下:
Event = collections.namedtuple('Event', 'time proc action')
在 Event 實例中,time 字段是事件發生時的仿真時間,proc 字段是出租車進程實例的編號,action 字段是描述活動的字符串。
下面逐行分析示例 16-20 中的 taxi_process 函數。
示例 16-20 taxi_sim.py:taxi_process 協程,實現各輛出租車的活動
def taxi_process(ident, trips, start_time=0): #1 """每次改變狀態時建立事件,把控制權讓給仿真器""" time = yield Event(start_time, ident, 'leave garage') #2 for i in range(trips): #3 time = yield Event(time, ident, 'pick up passenger') #4 time = yield Event(time, ident, 'drop off passenger') #5 yield Event(time, ident, 'going home') #6 # 出租車進程結束 #7
一、每輛出租車調用一次 taxi_process 函數,建立一個生成器對象,表示各輛出租車的運營過程。ident 是出租車的編號(如上述運行示例中的 0、一、2);trips 是出租車回家以前的行程數量;start_time 是出租車離開車庫的時間。
二、產出的第一個 Event 是 'leave garage'。執行到這一行時,協程會暫停,讓仿真主循環着手處理排定的下一個事件。須要從新激活這個進程時,主循環會發送(使用 send方法)當前的仿真時間,賦值給 time。
三、每次行程都會執行一遍這個代碼塊。
四、產出一個 Event 實例,表示拉到乘客了。協程在這裏暫停。須要從新激活這個協程時,主循環會發送(使用 send 方法)當前的時間。
五、產出一個 Event 實例,表示乘客下車了。協程在這裏暫停,等待主循環發送時間,而後從新激活。
六、指定的行程數量完成後,for 循環結束,最後產出 'going home' 事件。此時,協程最後一次暫停。仿真主循環發送時間後,協程從新激活;不過,這裏沒有把產出的值賦值給變量,由於用不到了。
七、協程執行到最後時,生成器對象拋出 StopIteration 異常。
爲了實例化 Simulator 類,taxi_sim.py 腳本的 main 函數構建了一個 taxis 字典,以下所示:
taxis = {i: taxi_process(i, (i + 1) * 2, i * DEPARTURE_INTERVAL) for i in range(num_taxis)} sim = Simulator(taxis)
DEPARTURE_INTERVAL 的值是 5;若是 num_taxis 的值與前面的運行示例同樣也是 3,這三行代碼的做用與下述代碼同樣:
taxis = {0: taxi_process(ident=0, trips=2, start_time=0), 1: taxi_process(ident=1, trips=4, start_time=5), 2: taxi_process(ident=2, trips=6, start_time=10)} sim = Simulator(taxis)
所以,taxis 字典的值是三個參數不一樣的生成器對象。例如,1 號出租車從start_time=5 時開始,尋找四個乘客。構建 Simulator 實例只需這個字典參數。Simulator.__init__ 方法如示例 16-22 所示。Simulator 類的主要數據結構以下。
self.events
PriorityQueue 對象,保存 Event 實例。元素能夠放進(使用 put 方法)PriorityQueue 對象中,而後按 item[0](即 Event 對象的 time 屬性)依序取出(使用 get 方法)。
self.procs
一個字典,把出租車的編號映射到仿真過程當中激活的進程(表示出租車的生成器對象)。這個屬性會綁定前面所示的 taxis 字典副本。
示例 16-22 taxi_sim.py:Simulator 類的初始化方法
class Simulator: def __init__(self, procs_map): self.events = queue.PriorityQueue() #1 self.procs = dict(procs_map) #2
一、保存排定事件的 PriorityQueue 對象,按時間正向排序。
二、獲取的 procs_map 參數是一個字典(或其餘映射),但是又從中構建一個字典,建立本地副本,由於在仿真過程當中,出租車回家後會從 self.procs 屬性中移除,而咱們不想修改用戶傳入的對象。
優先隊列是離散事件仿真系統的基礎構件:建立事件的順序不定,放入這種隊列以後,能夠按照各個事件排定的時間順序取出。例如,可能會把下面兩個事件放入優先隊列:
Event(time=14, proc=0, action='pick up passenger') Event(time=11, proc=1, action='pick up passenger')
這兩個事件的意思是,0 號出租車 14 分鐘後拉到第一個乘客,而 1 號出租車(time=10時出發)1 分鐘後(time=11)拉到乘客。若是這兩個事件在隊列中,主循環從優先隊列中獲取的第一個事件將是 Event(time=11, proc=1, action='pick uppassenger')。
下面分析這個仿真系統的主算法——Simulator.run 方法。在 main 函數中,實例化Simulator 類以後當即就調用了這個方法,以下所示:
sim = Simulator(taxis)
sim.run(end_time)
Simulator 類帶有註解的代碼清單在示例 16-23 中,下面先概述 Simulator.run 方法實現的算法。
(1) 迭表明示各輛出租車的進程。
a. 在各輛出租車上調用 next() 函數,預激協程。這樣會產出各輛出租車的第一個事件。
b. 把各個事件放入 Simulator 類的 self.events 屬性(隊列)中。
(2) 知足 sim_time < end_time 條件時,運行仿真系統的主循環。
a. 檢查 self.events 屬性是否爲空;若是爲空,跳出循環。
b. 從 self.events 中獲取當前事件(current_event),即 PriorityQueue 對象中時間值最小的 Event 對象。
c. 顯示獲取的 Event 對象。
d.獲取 current_event 的 time 屬性,更新仿真時間。
e.把時間發給 current_event 的 proc 屬性標識的協程,產出下一個事件(next_event)。
f.把 next_event 添加到 self.events 隊列中,排定 next_event。
示例 16-23 taxi_sim.py:Simulator,一個簡單的離散事件仿真類;關注的重點是run 方法
# -*- coding: utf-8 -*- import collections, queue, time, random NUM_TAXIS = 3 DEFAULT_END_TIME = 180 SEARCH_DURATION = 5 TRIP_DURATION = 20 DEPARTURE_INTERVAL = 5 Event = collections.namedtuple('Event', ['time', 'proc', 'action']) def taxi_process(ident, trips, start_time=0): #1 """每次改變狀態時建立事件,把控制權讓給仿真器""" time = yield Event(start_time, ident, 'leave garage') #2 for i in range(trips): #3 time = yield Event(time, ident, 'pick up passenger') #4 time = yield Event(time, ident, 'drop off passenger') #5 yield Event(time, ident, 'going home') #6 # 出租車進程結束 #7 taxis = {i: taxi_process(i, (i + 1) * 2, i * DEPARTURE_INTERVAL) for i in range(NUM_TAXIS)} class Simulator(object): def __init__(self, procs_map): self.events = queue.PriorityQueue() self.procs = dict(procs_map) def run(self, end_time): #1 """排定並顯示事件,直到時間結束""" # 排定各輛出租車的第一個事件 for _, proc in sorted(self.procs.items()): #2 first_event = next(proc) #3 self.events.put(first_event) #4 # 這個仿真系統的主循環 sim_time = 0 #5 while sim_time < DEFAULT_END_TIME: #6 if self.events.empty(): #7 print('*** end of events ***') break current_event = self.events.get() #8 在self.events中取出event後,該event就會從queue中刪除 sim_time, proc_id, previous_action = current_event #9 print('taxi: ', proc_id, proc_id * ' ', current_event) #10 active_proc = self.procs[proc_id] #11 next_time = sim_time + compute_duration(previous_action) #12 try: next_event = active_proc.send(next_time) #13 except StopIteration: del self.procs[proc_id] #14 else: self.events.put(next_event) #15 else: msg = '*** end of simulation time: {} events pending ***' print(msg.format(self.event.qsize())) def compute_duration(previous_action): """使用指數分佈計算操做的耗時""" if previous_action in ['leave garage', 'drop off passenger']: # 新狀態是四處徘徊 interval = SEARCH_DURATION elif previous_action == 'pick up passenger': # 新狀態是行程開始 interval = TRIP_DURATION elif previous_action == 'going home': interval = 1 else: raise ValueError('Unknown previous_action: %s' % previous_action) return int(random.expovariate(1 / interval)) + 1 sim = Simulator(taxis) sim.run(DEFAULT_END_TIME)
一、run 方法只須要仿真結束時間(end_time)這一個參數。
二、使用 sorted 函數獲取 self.procs 中按鍵排序的元素;用不到鍵,所以賦值給 _。
三、 調用 next(proc) 預激各個協程,向前執行到第一個 yield 表達式,作好接收數據的準備。產出一個 Event 對象。
四、 把各個事件添加到 self.events 屬性表示的 PriorityQueue 對象中。如示例16-20中的運行示例,各輛出租車的第一個事件是 'leave garage'。
五、 把 sim_time 變量(仿真鍾)歸零。
六、這個仿真系統的主循環:sim_time 小於 end_time 時運行。
七、若是隊列中沒有未完成的事件,退出主循環。
八、 獲取優先隊列中 time 屬性最小的 Event 對象;這是當前事件(current_event)。
九、拆包 Event 對象中的數據。這一行代碼會更新仿真鍾 sim_time,對應於事件發生時的時間。
十、顯示 Event 對象,指明是哪輛出租車,並根據出租車的編號縮進。
十一、從 self.procs 字典中獲取表示當前活動的出租車的協程。
十二、調用 compute_duration(...) 函數,傳入前一個動做(例如,'pick uppassenger'、'drop off passenger' 等),把結果加到 sim_time 上,計算出下一次活動的時間。
1三、把計算獲得的時間發給出租車協程。協程會產出下一個事件(next_event),或者拋出 StopIteration 異常(完成時)。
1四、若是拋出了 StopIteration 異常,從 self.procs 字典中刪除那個協程。
1五、 不然,把 next_event 放入隊列中。
1六、若是循環因爲仿真時間到了而退出,顯示待完成的事件數量(有時可能碰巧是零)。
注意,示例 16-23 中的 Simulator.run 方法有兩處用到了第 15 章介紹的 else 塊,並且都不在 if 語句中。一、主 while 循環有一個 else 語句,報告仿真系統因爲到達結束時間而結束,而不是因爲沒有事件要處理而結束。二、靠近主 while 循環底部那個 try 語句把 next_time 發給當前的出租車進程,嘗試獲取下一個事件(next_event),若是成功,執行 else 塊,把 next_event 放入self.events 隊列中。我以爲,若是沒有這兩個 else 塊,Simulator.run 方法的代碼會有點難以閱讀。這個示例的要旨是說明如何在一個主循環中處理事件,以及如何經過發送數據驅動協程。這是 asyncio 包底層的基本思想。