python協程2:yield from 從入門到精通

上一篇python協程1:yield的使用介紹了:python

  • 生成器做爲協程使用時的行爲和狀態git

  • 使用裝飾器預激協程github

  • 調用方如何使用生成器對象的 .throw(...) 和 .close() 方法控制協程編程

這一篇將介紹:併發

  • 協程終止時如何返回值函數

  • yield新句法的用途和語義ui

同時會用幾個協程的示例展現協程用法。spa

讓協程返回值

先看一個例子:
這段代碼會返回最終均值的結果,每次激活協程時不會產出移動平均值,而是最後一次返回。線程

#! -*- coding: utf-8 -*-

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
    # 返回一個namedtuple,包含count和average兩個字段。在python3.3前,若是生成器返回值,會報錯
    return Result(count, average)

咱們調用這段代碼,結果以下code

>>> coro_avg = averager()
>>> next(coro_avg)
>>> coro_avg.send(20) # 並無返回值
>>> coro_avg.send(30)
>>> coro_avg.send(40)
>>> coro_avg.send(None) # 發送None終止循環,致使協程結束。生成器對象會拋出StopIteration異常。異常對象的value屬性保存着返回值。
Traceback (most recent call last):
   ...
StopIteration: Result(count=3, average=30)

return 表達式的值會傳給調用方,賦值給StopIteration 異常的一個屬性。這樣作雖然看着彆扭,但爲了保留生成器對象耗盡時拋出StopIteration異常的行爲,也能夠理解。

若是咱們想獲取協程的返回值,能夠這麼操做:

>>> coro_avg = averager()
>>> next(coro_avg)
>>> coro_avg.send(20) # 並無返回值
>>> coro_avg.send(30)
>>> coro_avg.send(40)
>>> try:
...     coro_avg.send(None)
... except StopIteration as exc:
...     result = exc.value
...
>>> result
Result(count=3, average=30)

看到這咱們會說,這是什麼鬼,爲何獲取返回值要繞這麼一大圈,就沒有簡單的方法嗎?

有的,那就是 yield from

yield from 結果會在內部自動捕獲StopIteration 異常。這種處理方式與 for 循環處理StopIteration異常的方式同樣。
對於yield from 結構來講,解釋器不只會捕獲StopIteration異常,還會把value屬性的值變成yield from 表達式的值。

在函數外部不能使用yield from(yield也不行)。

既然咱們提到了 yield from 那yield from 是什麼呢?

yield from

yield from 是 Python3.3 後新加的語言結構。和其餘語言的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

list(gen())
['A', 'B', '1', '2']

能夠改寫爲:

def gen():
    yield from 'AB'
    yield from range(1, 3)
    

list(gen())
['A', 'B', '1', '2']

下面來看一個複雜點的例子:(來自Python cookbook 3 ,github源碼地址 https://github.com/dabeaz/python-cookbook/blob/master/src/4/how_to_flatten_a_nested_sequence/example.py)

# 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) # 這裏遞歸調用,若是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是可迭代對象。

PEP380 的標題是 」syntax for delegating to subgenerator「(把指責委託給子生成器的句法)。由此咱們能夠知道,yield from是能夠實現嵌套生成器的使用。

yield from 的主要功能是打開雙向通道,把最外層的調用方與最內層的子生成器鏈接起來,使二者能夠直接發送和產出值,還能夠直接傳入異常,而不用在中間的協程添加異常處理的代碼。

yield from 包含幾個概念:

  • 委派生成器

包含yield from <iterable> 表達式的生成器函數

  • 子生成器

從yield from <iterable> 部分獲取的生成器。

  • 調用方

調用委派生成器的客戶端(調用方)代碼

這個示意圖是 對yield from 的調用過程

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

這個圖來自於Paul

Sokolovsky 的 How Python 3.3 "yield from" construct works

下邊這個例子是對yield from 的一個應用:

#! -*- coding: utf-8 -*-

from collections import namedtuple


Result = namedtuple('Result', 'count average')


# 子生成器
# 這個例子和上邊示例中的 averager 協程同樣,只不過這裏是做爲字生成器使用
def averager():
    total = 0.0
    count = 0
    average = None
    while True:
        # main 函數發送數據到這裏 
        term = yield
        if term is None: # 終止條件
            break
        total += term
        count += 1
        average = total/count
    return Result(count, average) # 返回的Result 會成爲grouper函數中yield from表達式的值


# 委派生成器
def grouper(results, key):
     # 這個循環每次都會新建一個averager 實例,每一個實例都是做爲協程使用的生成器對象
    while True:
        # grouper 發送的每一個值都會經由yield from 處理,經過管道傳給averager 實例。grouper會在yield from表達式處暫停,等待averager實例處理客戶端發來的值。averager實例運行完畢後,返回的值綁定到results[key] 上。while 循環會不斷建立averager實例,處理更多的值。
        results[key] = yield from averager()


# 調用方
def main(data):
    results = {}
    for key, values in data.items():
        # group 是調用grouper函數獲得的生成器對象,傳給grouper 函數的第一個參數是results,用於收集結果;第二個是某個鍵
        group = grouper(results, key)
        next(group)
        for value in values:
            # 把各個value傳給grouper 傳入的值最終到達averager函數中;
            # grouper並不知道傳入的是什麼,同時grouper實例在yield from處暫停
            group.send(value)
        # 把None傳入groupper,傳入的值最終到達averager函數中,致使當前實例終止。而後繼續建立下一個實例。
        # 若是沒有group.send(None),那麼averager子生成器永遠不會終止,委派生成器也永遠不會在此激活,也就不會爲result[key]賦值
        group.send(None)
    report(results)


# 輸出報告
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, 41, 42, 43, 44, 54],
    'girls;m': [1.5, 1.6, 1.8, 1.5, 1.45, 1.6],
    'boys;kg':[50, 51, 62, 53, 54, 54],
    'boys;m': [1.6, 1.8, 1.8, 1.7, 1.55, 1.6],
}

if __name__ == '__main__':
    main(data)

這段代碼從一個字典中讀取男生和女生的身高和體重。而後把數據傳給以前定義的 averager 協程,最後生成一個報告。

執行結果爲

6 boys  averaging 54.00kg
6 boys  averaging 1.68m
6 girls averaging 44.00kg
6 girls averaging 1.58m

這斷代碼展現了yield from 結構最簡單的用法。委派生成器至關於管道,因此能夠把任意數量的委派生成器鏈接在一塊兒---一個委派生成器使用yield from 調用一個子生成器,而那個子生成器自己也是委派生成器,使用yield from調用另外一個生成器。最終以一個只是用yield表達式的生成器(或者任意可迭代對象)結束。

yield from 的意義

PEP380 分6點說明了yield from 的行爲。

  • 子生成器產出的值都直接傳給委派生成器的調用方(客戶端代碼)

  • 使用send() 方法發給委派生成器的值都直接傳給子生成器。若是發送的值是None,那麼會調用子生成器的 __next__()方法。若是發送的值不是None,那麼會調用子生成器的send()方法。若是調用的方法拋出StopIteration異常,那麼委派生成器恢復運行。任何其餘異常都會向上冒泡,傳給委派生成器。

  • 生成器退出時,生成器(或子生成器)中的return expr 表達式會觸發 StopIteration(expr) 異常拋出。

  • yield from表達式的值是子生成器終止時傳給StopIteration異常的第一個參數。

  • 傳入委派生成器的異常,除了 GeneratorExit 以外都傳給子生成器的throw()方法。若是調用throw()方法時拋出 StopIteration 異常,委派生成器恢復運行。StopIteration以外的異常會向上冒泡。傳給委派生成器。

  • 若是把 GeneratorExit 異常傳入委派生成器,或者在委派生成器上調用close() 方法,那麼在子生成器上調用close() 方法,若是他有的話。若是調用close() 方法致使異常拋出,那麼異常會向上冒泡,傳給委派生成器;不然,委派生成器拋出 GeneratorExit 異常。

yield from的具體語義很難理解,不過咱們能夠看下Greg Ewing 的僞代碼,經過僞代碼分析一下:

RESULT = yield from EXPR

# is semantically equivalent to
# EXPR 能夠是任何可迭代對象,由於獲取迭代器_i 使用的是iter()函數。
_i = iter(EXPR)
try:
    _y = next(_i) # 2 預激字生成器,結果保存在_y 中,做爲第一個產出的值
except StopIteration as _e:
    # 3 若是調用的方法拋出StopIteration異常,獲取異常對象的value屬性,賦值給_r
    _r = _e.value
else:
    while 1: # 4 運行這個循環時,委派生成器會阻塞,只能做爲調用方和子生成器直接的通道
        try:
            _s = yield _y # 5 產出子生成器當前產出的元素;等待調用方發送_s中保存的值。
        except GeneratorExit as _e:
            # 6 這一部分是用於關閉委派生成器和子生成器,由於子生成器能夠是任意可迭代對象,因此可能沒有close() 方法。
            try:
                _m = _i.close
            except AttributeError:
                pass
            else:
                _m()
            # 若是調用close() 方法致使異常拋出,那麼異常會向上冒泡,傳給委派生成器;不然,委派生成器拋出 GeneratorExit 異常。
            raise _e
        except BaseException as _e: # 7 這一部分處理調用方經過.throw() 方法傳入的異常。若是子生成器是迭代器,沒有throw()方法,這種狀況會致使委派生成器拋出異常
            _x = sys.exc_info()
            try:
                # 傳入委派生成器的異常,除了 GeneratorExit 以外都傳給子生成器的throw()方法。
                _m = _i.throw
            except AttributeError:
                # 子生成器一迭代器,沒有throw()方法, 調用throw()方法時拋出AttributeError異常傳給委派生成器
                raise _e
            else: # 8
                try:
                    _y = _m(*_x)
                except StopIteration as _e:
                     # 若是調用throw()方法時拋出 StopIteration 異常,委派生成器恢復運行。
                     # StopIteration以外的異常會向上冒泡。傳給委派生成器。
                    _r = _e.value
                    break
        else: # 9 若是產出值時沒有異常
            try: # 10 嘗試讓子生成器向前執行
                if _s is None: 
                    # 11. 若是發送的值是None,那麼會調用子生成器的 __next__()方法。
                    _y = next(_i)
                else:
                    # 11. 若是發送的值不是None,那麼會調用子生成器的send()方法。
                    _y = _i.send(_s)
            except StopIteration as _e: # 12
                # 2. 若是調用的方法拋出StopIteration異常,獲取異常對象的value屬性,賦值給_r, 退出循環,委派生成器恢復運行。任何其餘異常都會向上冒泡,傳給委派生成器。
                _r = _e.value 
                break
RESULT = _r #13 返回的結果是 _r 即整個yield from表達式的值

上段代碼變量說明:

  • _i 迭代器(子生成器)

  • _y 產出的值 (子生成器產出的值)

  • _r 結果 (最終的結果 即整個yield from表達式的值)

  • _s 發送的值 (調用方發給委派生成器的值,這個只會傳給子生成器)

  • _e 異常 (異常對象)

咱們能夠看到在代碼的第一個 try 部分 使用 _y = next(_i) 預激了子生成器。這能夠看出,上一篇咱們使用的用於自動預激的裝飾器與yield from 語句不兼容。

除了這段僞代碼以外,PEP380 還有個說明:

In a generator, the statement

return value

is semantically equivalent to

raise StopIteration(value)

except that, as currently, the exception cannot be caught by except clauses within the returning generator.

這也就是爲何 yield from 可使用return 來返回值而 yield 只能使用 try ... except StopIteration ... 來捕獲異常的value 值。

>>> try:
...     coro_avg.send(None)
... except StopIteration as exc:
...     result = exc.value
...
>>> result

到這裏,咱們已經瞭解了 yield from 的具體細節。下一篇,會分析一個使用協程的經典案例: 仿真編程。這個案例說明了如何使用協程在單線程中管理併發活動。

參考文檔

最後,感謝女友支持。

>歡迎關注 >請我喝芬達
歡迎關注 請我喝芬達
相關文章
相關標籤/搜索