Python異步編程之yield from

yield from 簡介

yield from 是Python3.3 後新加的語言結構,可用於簡化yield表達式的使用。
yield from 簡單示例:

>>> def gen():
...      yield from range(10)
...
>>> g = gen()
>>> next(g)
0
>>> next(g)
1
>>>

yield from 用於獲取生成器中的值,是對yield使用的一種優化。
yield from 兩個最重要的特點:

  1. 在調用包含yield from的函數時,程序會停在yield from 這裏,並將for循環的執行傳遞到子生成器裏面去。相當於直接調用子生成器。這個功能可以稱之爲傳輸通道
  2. 子生成器中的return,會被 res = yield from 捕獲,並賦值給res。這個可以稱之爲異常處理

傳輸通道

生成器存在這樣一種調用場景,有生成器A,生成器B調用A迭代取值。main函數從迭代生成器B獲取元素。這就是所謂的嵌套生成器。如果要迭代出最內層生成器的值,可以用如下方法:

>>> def sub_gen():
...     for i in range(5):
...         yield i
...     return 100
...
>>> def gen():
...     g = sub_gen()
...     while True:
...         try:
...             temp = next(g)
...             yield temp
...         except StopIteration as e:
...             print(f"sub_gen return {e.value}")
...             return
...
>>> g = gen()
>>> for i in g:
...     print(i)
...
0
1
2
3
4
sub_gen return 100

首先在外層生成器中使用while True 循環,通過next迭代內層生成器的值,然後捕獲異常獲取內層生成器的返回。

使用 yield from 不需要這兩個動作就能完成同樣的功能,有效減少代碼複雜度。

>>> def sub_gen():
...     for i in range(5):
...         yield i
...     return 100
...
>>> def gen():
...     g = sub_gen()
...     res = yield from g
...     print(f"sub_gen return:{res}")
...

>>> g = gen()
>>> for i in g:
...     print(i)
...
0
1
2
3
4
sub_gen return:100

執行流程:
當程序執行到 yield from 時,會暫停在這裏,將for循環作用到內層迭代器,也就是g = sub_gen()中的g變量。直到sub_gen執行到return拋出異常被yield from捕獲,這個調用算是結束。這就是yield from的傳輸通道

注意:除了可以通過yield from 傳輸通道的能力迭代取值,也可以通過send發送值到子生成器中

異常處理

在上一篇yield使用中說明過迭代生成器時遇到return會拋出異常,獲取返回值需要捕獲異常再取值,而yield from 的功能之二就是捕獲了異常,獲取到return的值,賦值給變量。

def sub_gen():
    for i in range(5):
        yield i  
    return 100


def gen():
    g = sub_gen()
    res = yield from g
    print(f"捕獲返回值:{res}")



def main():
    g = gen()
    for i in g:
        print(i)

main()

執行過程:
使用for循環迭代g,相當於for循環迭代sub_gen()。
sub_gen 生成器最後的返回值作爲異常拋出,調用方需要捕獲異常才能獲取返回值。但是有了yield from之後,sub_gen生成器的返回值異常就會被yield from捕獲,賦值給res變量。這就是yield from能夠處理內層生成器的返回值。這就是yield from的異常捕獲能力

yield from 專用術語

yield from使用的專門術語:
委派生成器:包含 yield from  表達式的生成器函數;即上面的gen生成器函數
子生成器:yield from 從中取值的生成器;即上面的sub_gen生成器函數
調用方:調用委派生成器的客戶端代碼;即上面的main生成器函數

三者之間的關係如下:

委派生成器在 yield from 表達式處暫停時,調用方可以直接迭代子生成器,子生成器把產出的值發給調用方。子生成器返回之後,解釋器會拋出StopIteration 異常,yield from會捕獲異常並取值,然後委派生成器會恢復。

yield from 實現的協程

在Python中有多種方式可以實現協程,例如:

  • greenlet 是一個第三方模塊,用於實現協程代碼(Gevent協程就是基於greenlet實現)
  • yield 生成器,藉助生成器的特點也可以實現協程代碼。
  • asyncio 在Python3.4中引入的模塊用於編寫協程代碼。
  • async & awiat 在Python3.5中引入的兩個關鍵字,結合asyncio模塊可以更方便的編寫協程代碼。

在Python3.4之前官方未提供協程的類庫,一般大家都是使用greenlet等其他來實現。在Python3.4發佈後官方正式支持協程,即:asyncio模塊。
在Python3.4-Python3.11的代碼中可以通過asyncio + yield from的方法來實現原生協程。

import time
import asyncio


@asyncio.coroutine
def task1():
    print('開始運行IO任務1...')
    yield from asyncio.sleep(2)  # 假設該任務耗時2s
    print('IO任務1已完成,耗時2s')
    return task1.__name__


@asyncio.coroutine
def task2():
    print('開始運行IO任務2...')
    yield from asyncio.sleep(3)  # 假設該任務耗時3s
    print('IO任務2已完成,耗時3s')
    return task2.__name__


@asyncio.coroutine
def main():

    # 把所有任務添加到task中
    tasks = [task1(), task2()]

    # 子生成器
    done, pending = yield from asyncio.wait(tasks)

    # done和pending都是一個任務,所以返回結果需要逐個調用result()
    for r in done:
        print(f'協程返回值:r.result()')


if __name__ == '__main__':
    start = time.time()
    # 創建一個事件循環對象loop
    loop = asyncio.get_event_loop()
    try:
        # 完成事件循環,直到最後一個任務結束
        loop.run_until_complete(main())
    finally:
        loop.close()
    print('所有IO任務總耗時%.5f秒' % float(time.time()-start))

代碼解釋:

  • @asyncio.coroutine 裝飾的生成器函數代表着一個任務
  • yield from asyncio.sleep(3) 模擬一個IO操作,協程遇到IO會自動切換
  • loop.run_until_complete(main()) 啓動一個事件循環,在循環中執行所有任務。任務遇到IO自動切換

輸出:

/Users/yield_from_demo.py:14: DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
  def task2():
/Users/yield_from_demo.py:22: DeprecationWarning: "@coroutine" decorator is deprecated since Python 3.8, use "async def" instead
  def main():
/Users/yield_from_demo.py:28: DeprecationWarning: The explicit passing of coroutine objects to asyncio.wait() is deprecated since Python 3.8, and scheduled for removal in Python 3.11.
  done, pending = yield from asyncio.wait(tasks)
開始運行IO任務1...
開始運行IO任務2...
IO任務1已完成,耗時2s
IO任務2已完成,耗時3s
協程返回值:r.result()
協程返回值:r.result()
所有IO任務總耗時3.00188秒

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