python實現協程(三)

一. 讓協程返回值

        下面的例子,我們再次改版之前計算平均值的協程函數,這一版本的協程函數每次被激活時,不會自動產出平均值,而是在最後返回一個值。(averager協程返回的結果是一個namedtuple,2個字段分別是count和average):

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)

        爲了返回值,協程必須正常終止,因此average中有個判斷條件,以便退出累計循環;返回值爲namedtuple,包含count和average兩個字段。(注:python3.3之前如果生成器返回值,解釋器會報句法錯誤)。 

演示1   發送None會終止循環,導致協程結束

注意:一如既往,協程結束,協程對象會拋出StopIteration異常。return表達式的值通過異常對象StopIteration傳遞給調用方。這樣做有點不合常理,但是能保留生成器對象的常規行爲——在耗盡時拋出StopIteration異常。

演示2  獲取協程return的值

捕獲StopIteration異常,並通過異常對象的value屬性獲取average返回的值。下面我們介紹的yield from結構會在內部自動捕獲StopIteration異常。這種處理方式與for循環處理StopIteration異常的方式一樣:循環機制使用用戶易於理解的方式處理異常。對yield from結構來說,解釋器不僅會捕獲StopIteration異常,還會把value屬性的值變爲yield from表達式的值。

二. 使用yield from

        首先要明確,yield from是全新的語法結構,其功能要比yield豐富。在python 3.5以後的版本中使用async、await關鍵字取代yield from,雖然語法上略有差異,但是原理是一樣的。理解了yield from,那麼async和await的使用自然水到渠成。

演示1  yield from可用於簡化for循環中的yield表達式

演示2 使用yield from鏈接可迭代對象

        yield from x表達式對x對象所做的第一件事就是調用iter(x),從中獲取迭代器。因此x可以是任何可迭代的對象。但,如果yield from結果唯一的作用是替代產出值的for循環,這個結果可能永遠不會被添加到python語言中。yield from結構的本質無法使用簡單的可迭代對象說明,要發散思維,使用嵌套生成器。

        yield from的主要功能是打開雙向通道,把最外層的調用方與最內層的子生成器連接起來,這樣二者可以直接發送和產出值,還可以直接傳入異常,而不用在位於中間的協程中添加大量處理異常的樣板代碼。在舉例前,我們先來了解幾個專門的術語:

  1. 委派生成器:包含yield from <iterable>表達式的生成器函數。
  2. 子生成器:從yield from表達式中<iterable>部分獲取的生成器。
  3. 調用方:調用委派生成器的客戶端代碼。在適當的語境中,將使用“客戶端”代指“調用方”,以便於委派生成器(也是調用方,因爲它調用了子生成器)區分開。

        委派生成器在yield from表達式處暫停時,調用方可以直接把數據發給子生成器,子生成器再把產出的值發給調用方。子生成器返回之後,解釋器會拋出StopIteration異常,並把返回的值附加到異常對象上,此時委派生成器會恢復。

示例  使用yield from計算平均值,並輸出統計報告

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])


# 客戶端代碼,即調用方
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)

運行結果:外層for循環每次迭代會產生一個grouper實例,賦值給group變量;group是委派生成器。調用next(group),預激委派生成器group,此時進入while循環,調用子生成器average後,在yield from表達式處暫停。內層for循環調用group.send(value),直接把值傳給子生成器average。同時,當前的group在yield from表達式處暫停。內層循環結束後,group實例依舊在yield from表達式處暫停。因此,grouper函數定義體中爲results[key]賦值的語句還沒有執行。如果外層for循環的末尾沒有group.send(None),那麼average子生成器永遠不會終止,委派生成器group永遠不會再次激活,因此永遠不會爲results[key]賦值。外層for循環重新迭代時會新建一個grouper實例,然後綁定到group變量上。前一個grouper實例(以及它創建的尚未終止的average子生成器實例)被垃圾回收機制回收。

注意

  1. group.send(None)以及average循環中的條件判斷是至關重要的終止條件。如果不這麼做,使用yield from調用這個協程的生成器會永遠阻塞;
  2. 返回的Result會成爲grouper函數中yield from表達式的值;
  3. grouper是委派生成器;
  4. 這個循環每次迭代時會新建一個average實例,每個實例都作爲協程使用的生成器對象。
  5. grouper發送的每個值都會經由yield from處理,通過管道傳給average實例。group會在yield from表達式處暫停,等待average實例處理客戶端發來的值。average實例運行完畢後,返回的值綁定到results[key]上。while循環會不斷創建average實例,處理更多的值。
  6. 把各個value傳給grouper,傳入的值最終到達averager函數中term = yield那一行;grouper永遠不知道傳入的值是什麼。
  7. 把None傳入grouper,導致當前的averager實例終止,也讓grouper繼續運行,再創建一個averager實例,處理下一組值。

這個實驗想表名的關鍵一點是,如果子生成器不終止,委派生成器會在yield from表達式處永遠暫停。如果是這樣,程序不會向前執行,因爲yield from把控制權交給客戶代碼(即委派生成器的調用方)了。

2.2 yield from的意義

  1. 子生成器產出的值都直接傳給委派生成器的調用方。
  2. 使用send方法發給委派生成器的值都直接傳給子生成器。如果發送的值是None,那麼會調用子生成器的__next__方法。如果發送的值不是None,那麼會調用子生成器的send方法。如果調用的方法拋出StopIteration異常,那麼委派生成器恢復運行。任何其它異常都會向上冒泡,傳給委派生成器。
  3. 生成器退出時,生成器中的return expr表達式會觸發StopIteration異常拋出。
  4. yield from表達式的值是子生成器終止時傳給StopIteration異常的第一個參數。

關於yield實現生成器的介紹至此已經完成,下一節我們將使用asyncio來實現協程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章