yield有兩個意思,一個是生產,一個是退讓,對於Python生成器的yield來說,這兩個含義都成立。yield這個關鍵字,既可以在生成器中產生一個值,傳輸給調用方,同時也可以從調用方那獲取一個值,在生成器內部使用。此外,yield還會作出讓步,暫停生成器,讓調用方繼續工作,直到調用方需要下一個數據時,調用方則陷入等待直到成器提供給調用方所需的數據,如此循環往復。乍一聽,有點像多線程,不明白多線程的同學也不要緊張,可以簡單的解釋一下多線程
解釋多線程之前,我們先解釋一下進程,進程可以看成是電腦里運行的一個實例,比方說,我運行一個瀏覽器,是一個進程,運行QQ,同樣也是進程,我用瀏覽器瀏覽網站,用瀏覽器聽音樂和下載東西,可以看成瀏覽器這個進程,里面有3個線程同時在為我做瀏覽網站,播放音樂還有下載文件。而我用QQ和人聊天,同時我又用QQ給人傳輸文件,同樣也是在QQ這個進程中,有兩個線程在為我傳輸聊天內容,同時傳輸文件。當然,上述說的並不嚴謹,只是為了好理解,因為對於像瀏覽器或者QQ這樣的進程,每時每秒可能有成百上千的線程在運行,有可能記錄日志或者其他
而協程相比於線程,最大的區別在於,協程不需要像線程那樣來回的中斷切換,也不需要線程的鎖機制,因為線程中斷或者鎖機制都會對性能問題造成影響,所以協程的性能相比於線程,性能有明顯的提高,尤其在線程越多的時候,優勢越明顯
下面用個例子來看一下協程的運作:
def simple_coroutine():
for i in range(3):
x = yield i + 1 # <1>
print("從調用方獲取的值:%s" % x)
my_coro = simple_coroutine() # <2>
first = next(my_coro) # <3>
for i in range(5): # <4>
try:
y = my_coro.send(i) # <5>
print("從生成器中獲取的值:%s" % y)
except StopIteration:
print("生成器的值拉取完畢") # <6>
print("生成器最初獲取的值:%s" % first)
運行結果:
從調用方獲取的值:0 從生成器中獲取的值:2 從調用方獲取的值:1 從生成器中獲取的值:3 從調用方獲取的值:2 生成器的值拉取完畢 生成器的值拉取完畢 生成器的值拉取完畢 生成器最初獲取的值:1
我們先來說一下程序的運行過程,先看程序中<2>處的代碼,傳統的概念中,我先執行了my_coro = simple_coroutine() 這塊代碼,所以會理所當然的認為, simple_coroutine() 這個方法要先執行完畢才能接着執行后續的代碼,但實際上不是,因為yield會標明這個方法是一個生成器,所以在程序的最初,他不會先執行完畢 simple_coroutine() 方法,而是把my_coro 這個變量聲明稱一個生成器,跳過simple_coroutine() ,接着執行后續代碼
Python將my_coro 聲明稱一個生成器后,調用了<3>處的next(my_coro) ,這個方法才開始會順序執行simple_coroutine()方法中的代碼,在Python解釋器執行simple_coroutine()方法時,遇到yield關鍵字,生成器將會陷入等待,這時候解釋器會跳到生成器之外,也就是<3>之后的代碼,我們調用生成器的send()方法,並傳輸一個值,這時候Python解釋器會從外部的代碼重新跳回simple_coroutine() 方法,中在之前停留的地方繼續執行
他會將外部傳來的值賦給x變量,並順序執行,直到遇到下一個yield,再像之前那樣跳出方法外
由於生成器能提供的值有限,所以當simple_coroutine()方法中執行了3次循環,生成器已經沒有多余的值可供調用方獲取了,所以每次調用生成器的send()方法,都會拋出StopIteration異常
這里有一點要注意,要激活一個生成器,一定要調用next()方法,而不是調用生成器的send()方法,如果直接調用send()方法會報錯
協程有四種狀態,分別是
GEN_CREATED:等待執行
GEN_RUNNING:解釋器執行
GEN_SUSPENDED:在yield表達式處暫停
GEN_CLOSED:執行結束
協程的狀態可以用inspect.getgeneratorstate()函數來確定,來看下面的例子:
from inspect import getgeneratorstate
from time import sleep
import threading
def get_state(coro):
print("其他線程生成器狀態:%s", getgeneratorstate(coro)) # <1>
def simple_coroutine():
for i in range(3):
sleep(0.5)
x = yield i + 1 # <1>
my_coro = simple_coroutine()
print("生成器初始狀態:%s" % getgeneratorstate(my_coro)) # <2>
first = next(my_coro)
for i in range(5):
try:
my_coro.send(i)
print("主線程生成器初始狀態:%s" % getgeneratorstate(my_coro)) # <3>
t = threading.Thread(target=get_state, args=(my_coro,))
t.start()
except StopIteration:
print("生成器的值拉取完畢")
print("生成器最后狀態:%s" % getgeneratorstate(my_coro)) # <4>
執行結果:
生成器初始狀態:GEN_CREATED 生成器狀態:%s GEN_SUSPENDED 生成器狀態:%s GEN_SUSPENDED 生成器的值拉取完畢 生成器的值拉取完畢 生成器的值拉取完畢 生成器最后狀態:GEN_CLOSED
<2>處,在激活協程之前,協程的狀態是GEN_CREATED,而執行next()之后,以及在調用生成器send()之間,我分主線程也就是調用方和多線程去觀察協程的狀態,結果狀態都是GEN_SUSPENDED,也就是協程處於暫停的狀態,我原本想用多線程去捕捉協程的運行態,結果即便是多線程捕捉協程也是GEN_SUSPENDED,而GEN_RUNNING也說明,只有帶解釋器在運行協程的時候,協程的狀態才是GEN_RUNNING,最后是GEN_CLOSED,我們拉取完協程的值后,協程的狀態就變為執行結束
示例:使用協程計算平均值
我們可以開發一個協程,不斷的往協程發送值,並且讓協程累計之前的值並計算平均值,如下:
from functools import wraps
def coroutine(func):
@wraps(func)
def primer(*args, **kwargs):
gen = func(*args, **kwargs)
next(gen)
return gen
return primer
@coroutine # <1>
def averager():
total = .0
count = 0
average = None
while True:
term = yield average
total += term
count += 1
average = total / count
try:
coro_avg = averager()
print(coro_avg.send(10))
print(coro_avg.send(20))
print(coro_avg.send(30))
coro_avg.close() # <2>
print(coro_avg.send(40))
except StopIteration:
print("協程已結束")
運行結果:
10.0 15.0 20.0 協程已結束
在<1>處,我們用一個裝飾器來預先激活協程,而不是之后再調用方里執行一個next()函數。然后,我們不斷往協程里傳10、20、30,而協程不斷累計傳入的值,並計算所有值的平均值返回給調用方,最后,我們在<2>處調用協程的close()函數,關閉協程,再調用send()方法,會發現拋出StopIteration異常
當發送給協程不是數字,會導致協程內部有異常拋出
for i in range(1, 6):
try:
print(coro_avg.send(i))
if i % 3 == 0:
coro_avg.send('')
except StopIteration:
print("協程已結束")
except TypeError:
print("傳入值異常")
運行結果:
1.0 1.5 2.0 傳入值異常 協程已結束 協程已結束
我們設置,當i為3的時候,多傳入一個空字符串,結果協程拋出類型錯誤,協程將運行狀態改為結束,之后再往協程傳值,都拋出StopIteration異常
我們可以讓協程處理一些特定的異常,比如:
class DemoException(Exception): # <1>
pass
def demo_exec_handling():
print("coroutine started")
while True:
try:
x = yield # <2>
except DemoException: # <3>
print("DemoException handled")
else:
print("coroutine received:{}".format(x))
運行結果
>>> exec_coro = demo_exec_handling() >>> next(exec_coro) coroutine started >>> print(exec_coro.send(1)) coroutine received:1 None >>> exec_coro.send(2) coroutine received:2 >>> exec_coro.send(3) coroutine received:3 >>> exec_coro.throw(DemoException) DemoException handled >>> exec_coro.send(4) coroutine received:4 >>> exec_coro.throw(ZeroDivisionError) Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 5, in demo_exec_handling ZeroDivisionError >>> exec_coro.send(5) Traceback (most recent call last): File "<stdin>", line 1, in <module> StopIteration
<1>處的DemoException是用來測試協程的,首先我們可以看到,當yield右邊沒有任何式子時,返回給調用方的是一個None對象,其次如果我們調用throw()方法將異常傳入協程,因為協程里有關於DemoException的捕捉,所以協程會繼續執行,當我們繼續傳入ZeroDivisionError,則協程結束
讓協程返回值
我們可以改造之前的averager()函數,使它可以返回一個對象,對象里有count和average兩個屬性
from collections import namedtuple
Result = namedtuple("Result", ["count", "average"])
def averager():
total = .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)
>>> coro_avg.send(10)
10.0
>>> coro_avg.send(20)
15.0
>>> coro_avg.send(30)
20.0
>>> coro_avg.send(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration: Result(count=3, average=20.0)
當我們發送None的時候,協程結束,返回結果,一如既往,生成器會拋出StopIteration異常,異常對象的value屬性保存着返回值,為了獲取返回值,我們還要再修改一下代碼
try:
coro_avg = averager()
next(coro_avg)
coro_avg.send(10)
coro_avg.send(20)
coro_avg.send(30)
coro_avg.send(None)
except StopIteration as exc:
result = exc.value
print(result)
運行結果:
Result(count=3, average=20.0)
結語:關於協程yield結構這一塊,到此暫做結束,下一章會介紹協程的yield from結構,yield from結構會在內部自動捕獲StopIteration異常,還會把協程的返回值變成yield from表達式的值,下一章節將會討論yield from的結構和用法,謝謝大家
