轉載自我自己的 github 博客 ——> 半天鐘的博客
這篇博文講述的 python 協程是 不正式的、寬泛的協程 ,即通過客戶調用 .send(…) 方法發送數據或使用 yield from 結構驅動的生成器函數, 而不是 asyncio 庫采用的定義更為嚴格的協程。
前言
在 事件驅動型編程 中,協程常用于離散事件的仿真(在單個線程中使用一個主循環驅動協程執行并發活動)。
協程通過顯式 自主地把控制權讓步給中央調度程序 從而實現了 協作式多任務 。
所以, 協程是 python 事件驅動型框架和協作式多任務的基礎。
那么,弄明白協程的 進化過程 、基本行為和 高效的使用方式 是很有必要的。
本博文想要解釋清楚 python 協程的基本行為以及如何高效的使用協程。
在閱讀本文之前,你必須要了解 python 中 yield 關鍵字、和生成器的基本概念。如果你還不知道這兩個概念是啥,你可以看我的上一篇博文:淺析 python 迭代器與生成器 或者通過 CSDN 上馮爽朗的博文 簡單了解 yield 關鍵字的使用方法。
從生成器到協程
協程是指一個過程,這個過程與調用方協作,即 根據調用方提供的值 產出相應的值 給調用方。
從協程的定義來看,協程的部分行為和帶有 yield 關鍵字生成器的行為類似,因為調用方可以使用 .next() 方法 讓生產器產出值給調用方。例如,這個斐波那契生成器函數:
>>
>
def
fibonacci
(
)
:
a
,
b
=
0
,
1
while
True
:
yield
a
a
,
b
=
b
,
a
+
b
調用方調用 next()函數可以 獲取它的產出值 :
>>
>
f
=
fibonacci
(
)
>>
>
print
(
next
(
f
)
)
0
>>
>
print
(
next
(
f
)
)
1
這么看來, 生成器的行為離協程的行為就差一步,即接收調用方提供的值。
在 python 2.5 后 yield 關鍵字就可以在表達式中使用了,而且生成器 API 中增加了 .send(value)方法。 生成器的調用方可以使用 .send(…) 方法給生成器發送數據。
這樣一來生成器就可以接收調用方提供的值了, 其接收的數據會成為 yield 表達式的值。
例一是一個簡單的例子,來說明調用方如何發送數據及生成器如何接受數據。
>>
>
def
coroutine
(
)
:
print
(
'-- 協程開始 --'
)
x
=
yield
'Nothing'
print
(
'-- 協程接收到了數據: {!r} -- '
.
format
(
x
)
)
>>
>
coro
=
coroutine
(
)
<
generator
object
coroutine at
0x10bbb2408
>
>>
>
next
(
coro
)
-
-
協程開始
-
-
Nothing
>>
>
coro
.
send
(
77
)
-
-
協程接收到了數據
:
77
-
-
Traceback
(
most recent call last
)
:
.
.
.
StopIteration
上面的例子表明:
- 在協程中, yield 通常出現在表達式的右邊。
- 調用方先使用一次 .next() 執行 yield ‘Nothing’ 讓協程產出字符串 “Nothing” 并 懸停至至 yield 表達式這一行
- 調用方使用 .send() 發送數據給協程。
- 發送的 數據代替 yield 表達式 ,并賦給變量 x。
- 協程結束時與生成器一致,都會拋出 StopIteration 異常。
需要特別注意的地方有:
首先、調用方只有在協程停在了 yield 表達式時,才能調用 .send() 發送數據
,否則,協程會拋出 TypeError 異常,如例二:
>>
>
coro
=
coroutine
(
)
>>
>
coro
.
send
(
77
)
Traceback
(
most recent call last
)
:
.
.
.
in
coro
.
send
(
77
)
TypeError
:
can't send non
-
None
value to a just
-
started generator
懸停在 yield 表達式的協程狀態是
GEN_SUSPENDED
,你可以使用inspect.getgeneratorstate(...)
函數確定協程的狀態。
其次、調用方使用 .send(y) 發送的數據會代替協程中的 yield 表達式 ,在上例中,發送的數據 y 是 77 ,77 代替了 yield 表達式,并賦給了變量 x。
最后、當賦值完畢后、協程會繼續前進至下一個 yield 關鍵字并懸停 ,直至結束從而拋出 StopIteration 異常。
你可以把 .send( y ) 看做兩個部分的結合,即:
- yield 表達式 = y
- .next()
這樣一來,擁有 .send()方法的生成器,完全符合了協程的定義,它可以通 過 .send() 接受調用方傳遞的值,并且可以通過 yield 產出值給調用方。
不過,此時我們沒有辦法在一創建協程時,立馬使用它。
你必須要先使用一次 .next() 讓協程懸停在 yield 表達式那一行,從而使協程轉變至
GEN_SUSPENDED 狀態
。這樣的行為被稱作
預激協程。
預激協程
毫無疑問,預激協程是一個很容易被遺忘的步驟。
需要使用 .send() 發送數據之前還必須使用一次 .next(),這讓人感到厭煩。
我們有什么辦法能夠自動預激協程呢?
有一種方法是使用能夠提前調用一次 .next() 的裝飾器,如下面這個 coroutine 裝飾器:
# BEGIN CORO_DECO
>>
>
from
functools
import
wraps
>>
>
def
coroutine_deco
(
func
)
:
"""Decorator: primes `func` by advancing to first `yield`"""
@wraps
(
func
)
#使用 functools.wraps 裝飾器獲得源 func 的所有參數 "*args,**kwargs"
def
primer
(
*
args
,
**
kwargs
)
:
gen
=
func
(
*
args
,
**
kwargs
)
#使用源生成器函數獲取生成器
next
(
gen
)
#調用 .next 方法
return
gen
#返回調用 .next 方法后的生成器
return
primer
# END CORO_DECO
網上有多個類似的裝飾器。這個改自 ActiveState 中的一個訣竅——Pipeline made of coroutines,作者是 Chaobin Tang,而他是受到了 David Beazley 的啟發。—— 《流暢的 python 》
使用這個裝飾器后,現在我們再運行例二的代碼就不會報 TypeError 異常,而是會正常運行了,如下:
@coroutine_deco
>>
>
def
coroutine
(
)
:
print
(
'-- 協程開始 --'
)
x
=
yield
'Nothing'
print
(
'協程接收到了數據: {!r}'
.
format
(
x
)
)
>>
>
coro
=
coroutine
(
)
-
-
協程開始
-
-
>>
>
import
inspect
>>
>
inspect
.
getgeneratorstate
(
coro
)
GEN_SUSPENDED
>>
>
cro
.
send
(
77
)
協程接收到了數據
:
77
Traceback
(
most recent call last
)
:
.
.
.
StopIteration
>>
>
inspect
.
getgeneratorstate
(
coro
)
GEN_CLOSED
該例子有如下行為需要注意:
- 在創建協程 coro 對象后,直接輸出了 “-- 協程開始 --” 字符串,這表明, 在創建協程對象后,其自動調用了一次 next() 方法。
-
使用
inspect.getgeneratorstate
查看協程的狀態,發現其已經是GEN_SUSPENDED
狀態, 說明協程內部已經懸停在 yield 關鍵字處。 - 能夠直接調用 .send() 方法而不用事先使用 .next() 了。
-
協程結束時的狀態是
GEN_CLOSED
協程還有一個很常用的方法 —— .close() 用于提前關閉協程。使用該方法后,協程會在 yield 表達式那一行拋出 GeneratorExit 異常。
有時,我們需要協程在結束了所有工作時,返回一個值, 這在 python 3.3 之前是不可能的,因為在協程的方法體中寫 return 關鍵字會報句法錯誤。
讓協程在終止時返回值
我們可以在 python 3.3 及之后的版本中 讓終止的協程返回想要的值 ,只是獲取返回值的方法比較曲折。
下面的例三,定義了一個動態計算平均值的協程,并讓其在結束工作(接受到 None 值)后 返回一個元組 ,該元組保存著目前為止收到的數據個數以及最終的平均值。
>>
>
from
collections
import
namedtuple
>>
>
Result
=
namedtuple
(
'Result'
,
'count average'
)
>>
>
def
averager
(
)
:
total
=
0.0
count
=
0
average
=
None
while
True
:
term
=
yield
average
if
term
is
None
:
break
total
+=
term
count
+=
1
average
=
total
/
count
return
Result
(
count
,
average
)
該函數有以下行為:
>>
>
coro_avg
=
averager
(
)
>>
>
next
(
coro_avg
)
# <1>
>>
>
coro_avg
.
send
(
10
)
# <2>
10.0
>>
>
coro_avg
.
send
(
30
)
20.0
>>
>
coro_avg
.
send
(
6.5
)
15.5
>>
>
coro_avg
.
send
(
None
)
# <3>
Traceback
(
most recent call last
)
:
.
.
.
StopIteration
:
Result
(
count
=
3
,
average
=
15.5
)
注釋:
① : 手動預激協程。
② : 調用 .send(10) 返回目前傳入所有數的平均值10、之后每傳入一個數都能實時計算所有數的平均值。
③ : 傳入 None ,手動結束該協程。
注意到,和往常一樣,結束后 協程拋出了 StopIteration 異常 。不一樣的是, 該異常保存著返回的值 ,即 Result 對象。
return 表達式的值會偷偷傳給調用方,賦值給 StopIteration 異常的一個屬性。這樣做有點不合常理,但是能 保留生成器對象的常規行為 ——耗盡時拋出 StopIteration 異常。
改造上面的代碼,手動捕獲異常,獲取返回值,可以這樣寫:
>>
>
coro_avg
=
averager
(
)
>>
>
next
(
coro_avg
)
>>
>
coro_avg
.
send
(
10
)
10.0
>>
>
coro_avg
.
send
(
30
)
20.0
>>
>
coro_avg
.
send
(
6.5
)
15.5
>>
>
try
:
coro_avg
.
send
(
None
)
except
StopIteration
as
exc
:
result
=
exc
.
value
>>
>
result
Result
(
count
=
3
,
average
=
15.5
)
目前,我們說明了如何讓 生成器接收調用方提供的值從而進化成協程 、如何 使用裝飾器自動預激協程 、以及 如何從協程獲取看起來很有用的返回值。
使用協程似乎太麻煩了點 !
不是嗎? 為了避免麻煩,我們必須自己定義一個自動預激協程的裝飾器,為了獲取協程的返回值,我們還必須捕捉異常,并獲取異常的 value 屬性。
有什么辦法能夠消除這些麻煩呢?(不用自定義預激裝飾器也不用捕獲異常以獲得返回值)
在 python 3.3 以后,有一個新的句法能夠幫助我們解決這些麻煩,即 yield from
yield from 及其工作原理
使用 yield from 關鍵字 不僅能自動預激協程 、 自動提取異常的 value 屬性返回值作為 yield from 表達式的值 ,還能夠 作為調用方和協程之間的通道 。
如果將例三中的 averager() 改編成使用 yield from 關鍵字來實現,會是例四的代碼:
>>
>
from
collections
import
namedtuple
>>
>
Result
=
namedtuple
(
'Result'
,
'count average'
)
>>
>
def
averager
(
)
:
total
=
0.0
count
=
0
average
=
None
while
True
:
term
=
yield
average
if
term
is
None
:
break
total
+=
term
count
+=
1
average
=
total
/
count
return
Result
(
count
,
average
)
>>
>
result
=
set
(
)
# <1>
>>
>
def
yf_averager
(
result
)
:
# <2>
while
True
:
# <3>
r
=
yield
from
averager
(
)
# <4>
result
.
add
(
r
)
>>
>
yfa
=
yf_averager
(
result
)
# <5>
>>
>
next
(
yfa
)
# <6>
>>
>
yfa
.
send
(
10
)
# <7>
10.0
>>
>
yfa
.
send
(
30
)
20.0
>>
>
yfa
.
send
(
6.5
)
15.5
>>
>
yfa
.
send
(
None
)
# <8>
>>
>
result
# <9>
{
Result
(
count
=
3
,
average
=
15.5
)
}
在例四中,averager() 方法并沒有做任何改變
解釋:
①:創建 result 集合以在調用方收集結果。
②:yield from 關鍵字的
載體函數
,有時也叫“委派生成器” ,設立這一函數是因為
在函數外部使用 yield from(以及 yield)會導致句法錯誤。
③:使用循環以保證傳入 None 時
yf_averager 生成器不拋出 StopIteration 異常
從而直接結束整個程序,若是如此,我們便觀察不到 result 了。
④:使用 yield from 關鍵字后面是
協程
、前面是接收協程最終返回值的變量 r,這個 r 我們最終會放在全局變量 result 集合中。還有一點需要注意、
當函數體重含有 yield from 那么它本身就是協程了
。
⑤:新建 yf_averager 協程,以
建立調用方與 averager 協程的通道
⑥:預激 yf_averager 協程
⑦:使用 .send()發送數據
⑧:發送 None 以結束 averager 協程
⑨:展示 result 集合中的值,確認接收到了最終的結果
上面如果上面這個例子你不怎么看得懂,沒關系,我會在后面解釋。
你現在
只需要知道 yield from 有這些行為:
- 在例四中,我們沒有預激 averager 協程,但是它能夠正常工作。 這說明 yield from 關鍵字會自動預激協程。
- 調用方使用委派生成器 yf_averager 傳入的值會送到 averager 里,并且調用方可以接收到 averager 協程處理后返回的值。 這說明了使用 yield from 的委派生成器 yf_averager 可以在調用方和協程之間建立通道,傳輸數據。
- 在獲取 averager 結果時,我們沒有捕獲異常,而是在第 22 行代碼中將返回值直接賦給了變量 r。 這說明了協程的最終返回值會成為 yield from 表達式的值。
yield from 關鍵字的原理
接下來這段偽碼等效于 RESULT = yield from EXPR 語句 。它能夠幫助你理解例四中 yield from 的行為
這并不是完整的偽代碼,它去除了 .throw()和 .close()方法,只處理 StopIteration 異常。完整的偽碼在這里 -> yield_from_expansion,不過在理解其功能的方面上,這足夠了。
_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
# <8>
解釋:
① :EXPR 可以是任何可迭代的對象,因為獲取迭代器 _i(這是子生成器,例子中的 averager 協程)使用的是 iter() 函數。
② :
預激子生成器(averager 協程);結果保存在 _y 中,作為產出的第一個值。
③ :如果拋出 StopIteration 異常,
獲取異常對象的 value 屬性,賦值給 _r
——這是最簡單情況下的返回值(RESULT)。
④ :運行這個循環時,委派生成器(yf_averager 生成器)會阻塞,
只作為調用方和子生成器之間的通道
。
⑤ :**產出子生成器當前產出的元素;等待調用方發送 _s 中保存的值。**因為這一個 yield 表達式和 ⑥ 中的send(),
委派生成器也變成了協程。
⑥ :嘗試讓子生成器向前執行,
轉發調用方發送的 _s
。
⑦ :如果子生成器拋出 StopIteration 異常,
獲取 value 屬性的值,賦值給 _r
,然后退出循環,讓委派生成器恢復運行。
⑧ :
返回的結果(RESULT)是 _r
,即整個 yield from 表達式的值。
以上的偽代碼和注釋,幾戶原封不動的搬了《流程的 python 》里的解釋,我只是增加了一些注釋。因為我想不出如何更好的總結 yield from 關鍵字的原理。
注意,因為 yf_averager 是帶 yield 關鍵字的生成器,所以在 ⑧ 結束后, 若找不到下一個 yield 關鍵字,那么 yf_averager 生成器會拋出 StopIteration 異常 ,這是我在例四中設立 while 循環 ③ 的直接原因。
我建議你在看懂這段偽代碼的基礎上再去 回顧例四 ,這下你 應該豁然開朗 了。如果還看不懂的話,我建議你多花些時間去看《流程的 python 》的第十六章,該章用了60多頁的篇幅把 python 協程講得很通透。
結語
本篇博文中,我用了四個小節敘述了我理解中的協程、及其使用技巧。在一開始,我講述了 協程是什么 ,及 如何在 python 2.2 及以后的版本中用生成器構建協程 ;然后我講述了 協程的必要操作(預激)的自動化方法 和 如何在 python 3.3 及以后的版本中獲取協程的返回值 ;最后,我講述了方便的 yield from 關鍵字的用法、行為 以及 它的主要原理 。
如果你想要知道 協程的具體用處 ,《流程的 python 》的第十六章中舉了一個離散事件仿真的例子—— 出租車隊運營仿真 。該仿真程序會創建幾輛出租車,并模擬他們并行運作(離開倉庫、尋找乘客、乘客下次、四處徘徊、回家)。對于說明如何使用協程做離散事件仿真是一個很好的例子。
這是那個出租車隊運營仿真例子的源碼 -> taxi_sim
我希望你看完這篇博文后能夠有所收獲、如果你看到了一些錯誤,請在評論中指出。
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
