Python協程詳解(一)


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的結構和用法,謝謝大家

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM