Python 異步編程之yield關鍵字

image

背景介紹

在前面的篇章中介紹了同步和異步在IO上的對比,從本篇開始探究python中異步的實現方法和原理。
python協程的發展流程:

  • python2.5 爲生成器引用.send()、.throw()、.close()方法
  • python3.3 爲引入yield from,可以接收返回值,可以使用yield from定義協程
  • Python3.4 加入了asyncio模塊
  • Python3.5 增加async、await關鍵字,在語法層面的提供支持
  • python3.7 使用async def + await的方式定義協程
  • 此後asyncio模塊更加完善和穩定,對底層的API進行的封裝和擴展
  • python將於 3.10版本中移除 以yield from的方式定義協程

yield 簡介

yield 通常是使用在生成器函數中。當一個函數中有yield關鍵字,那麼該函數就不是一個普通函數而是一個生成器函數。

>>> def get_num():
...     for i in range(5):
...         yield i
...
>>> g = get_num()
>>> type(g)
<class 'generator'>
>>>
>>> for i in g:
...     print(i)
...
0
1
2
3
4

調用get_num生成了一個生成器g,通過type可以看到g是一個生成器類型。生成器是一種迭代器,可以通過for循環迭代出數據。

以上是yield的第一種使用方法,其實yeild出了可以作爲生成器的關鍵字,也可以實現協程。在實現協程之前首先需要學習yield的基礎使用。

next 取值

yield實現的生成器是一種迭代器,所有的迭代器都可以通過next取值。

>>> def get_num():
...     for i in range(5):
...         yield i
...
>>> g = get_num()
>>>
>>> next(g)
0
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
4
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
>>>

執行過程
使用next會讓get_num從頭開始執行,遇到yield i時,返回i給調用者,並暫停在yield i這一行,等待下一個next取值的到來,從yield i這裏繼續執行。這種能夠暫停執行,恢復執行的能力是生成器的一種特性,也就是這種特性可以實現協程。
特點:
使用next取值有兩個特點:

  1. 只能從前向後取值,每次只能取一個
  2. 迭代器中沒有值時再通過next取值會報錯

send 發送值

調用包含yield的函數會返回一個生成器generator,可以通過next從生成器中不斷取值,通過send也可以將數值送到生成器中。如下:

>>> def get_num():
...     for i in range(5):
...         temp = yield i
...         print(temp)
...
>>> g = get_num()
>>> next(g)
0
>>> g.send(100)
100
1
>>>

執行過程
next讓程序執行到temp = yield i,返回i給調用者並暫停在這裏。g.send(100) 從temp = yield i開始執行,將100傳遞給yield i,並讓代碼繼續執行直到遇到下一個yield,返回yield 後面的數值。

send可以將值傳遞給生成器,next是從生成器中取值,兩者目的不一致,但是也相同的能力,那就是可以驅動程序從一個yield執行到下一個yield。如再次執行send,就會從上一次暫停的地方繼續執行到下一個yield處。

>>> g = get_num()
>>> next(g)
0
>>> g.send(100)
100
1
>>> g.send(200)
200
2

特點

  1. 將值傳送到生成器中
  2. 驅動生成器執行

啓動生成器

生成器創建之後需要啓動才能返回值,也就從代碼第一行執行到yield處,需要一個事件去驅動代碼執行。兩種方式可以啓動,分別是send和next

>>> g = get_num()
>>> next(g)
0
>>> g = get_num()
>>> g.send(None)
0

next:
程序從第一行執行到 temp = yield i暫停。
send:
send()必須傳入關鍵字None,其他值會報錯。因爲send是從yield 處開始執行,由於啓動程序不是yield語句開始,所有不能傳值。
close 結束迭代
通常來說不要手動接受生成器,因爲生成器迭代完成之後就會被釋放。但是也可以通過close的方法結束生成器的迭代。

>>> g = get_num()
>>> g.send(None)
0
>>>
>>>
>>>
>>> g = get_num()
>>>
>>> next(g)
0
>>> next(g)
None
1
>>> g.send(100)
100
2
>>> g.close()
>>> g.close()
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

生成器的return

在前面的示例中或許你也發現了,使用next取值,當取完所有元素之後再次取值時會拋出異常。根本原因是程序執行到return了。在生成器中執行到return會拋出StopIteration異常

>>> def gen():
...     yield 100
...     return 200
...     yield 300
...
>>> g = gen()
>>> next(g)
100
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration: 200
>>> next(g)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration    
程序執行到return時會拋出StopIteration異常,終止程序。yield 300永遠也不會執行。
調用生成器中無法直接獲取到return的返回值,可以通過捕獲異常的方法獲取return的返回值。
>>> try:
...     next(g)
...     next(g)
... except StopIteration as e:
...     result = e.value
...     print(result)
...
100
200

使用try except 捕獲 StopIteration 異常之後,從中取出value就是返回值。

生成器實現的協程

生成器通常用作迭代器,但是也可以用作協程。協程其實就是生成器函數,通過主體中含有 yield 關鍵字的函數創建。注意這裏只是爲了探究原理,真實情況下協程不是使用yield。
協程的類似於函數調用,函數A調用函數B,B執行完成之後A繼續執行。這個過程不涉及CPU調度。
下面通過生產者,消費者模型來說明yield如何實現協程。

import time


def consume():
    r = ''
    while True:
        n = yield r
        print(f'[consumer] 開始消費 {n}...')
        time.sleep(1)
        r = f'{n} 消費完成'


def produce(c):
    next(c)
    n = 0
    while n < 5:
        n = n + 1
        print(f'[producer] 生產了 {n}...')
        r = c.send(n)
        print(f'[producer] consumer return: {r}')
    c.close()


if __name__=='__main__':
    c = consume()
    produce(c)

執行輸出:

[producer] 生產了 1...
[consumer] 開始消費 1...
[producer] consumer return: 1 消費完成
[producer] 生產了 2...
[consumer] 開始消費 2...
[producer] consumer return: 2 消費完成
[producer] 生產了 3...
[consumer] 開始消費 3...
[producer] consumer return: 3 消費完成
[producer] 生產了 4...
[consumer] 開始消費 4...
[producer] consumer return: 4 消費完成
[producer] 生產了 5...
[consumer] 開始消費 5...
[producer] consumer return: 5 消費完成

執行過程:

  1. c = consume() 創建消費者生成器
  2. produce(c)將消費者生成器傳遞到生產者函數中,生產者會負責驅動消費者
  3. next(c)驅動生產者啓動,send(None)也可以完成
  4. 生產者在while中自增n,並調用 r = c.send(n) 將n傳遞給消費者
  5. 消費者n = yield r接收到n,用time.sleep模擬睡眠,給返回值r賦值,運行到下一個n = yield r暫停,返回r給生產者
  6. 生產者從暫定的r = c.send(n)恢復執行
  7. 直到n<5,生產者退出之後,整個協程退出。

這個過程中,主要配合的就是r = c.send(n)和 n = yield r這兩個關鍵點。兩行代碼在執行時可以暫停,驅動另外一個執行。這裏體現的協程的一個特點:主動讓出CPU,協助式執行而不是線程那種CPU搶佔式。
image

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