欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Python協(xié)程原理全面分析

 更新時(shí)間:2023年02月08日 10:46:30   作者:lijiachang8  
協(xié)程(co-routine,又稱微線程、纖程)是一種多方協(xié)同的工作方式。協(xié)程不是進(jìn)程或線程,其執(zhí)行過程類似于Python函數(shù)調(diào)用,Python的asyncio模塊實(shí)現(xiàn)的異步IO編程框架中,協(xié)程是對(duì)使用async關(guān)鍵字定義的異步函數(shù)的調(diào)用

序章

yield item這行代碼會(huì)產(chǎn)出一個(gè)值,提供給next()的調(diào)用方;此外還會(huì)做出讓步,暫停執(zhí)行生成器,讓調(diào)用方繼續(xù)工作,知道需要使用另一個(gè)值再調(diào)用next()。調(diào)用方會(huì)從生成器中拉取值。

從語(yǔ)法上來(lái)看,協(xié)程與生成器類似,都是從定義體中包含yield關(guān)鍵字的函數(shù)??墒?,在協(xié)程中,yield通常出現(xiàn)在表達(dá)式的右邊(如data = yield),可以產(chǎn)出值,也可以不產(chǎn)出:如果yield關(guān)鍵字后面沒有表達(dá)式,那么生成器產(chǎn)出None。協(xié)程可能會(huì)從調(diào)用方接收數(shù)據(jù),不過調(diào)用方把提供數(shù)據(jù)給協(xié)程使用的方式是.send(data)方法。調(diào)用方會(huì)把值推送給協(xié)程。

yield關(guān)鍵字甚至可以不接受或傳出數(shù)據(jù)。不管數(shù)據(jù)如何流動(dòng),yield都是一種流程控制工具,使用它可以實(shí)現(xiàn)協(xié)作式多任務(wù);協(xié)程可以把控制器讓步給中心調(diào)度程序,從而激活其他的協(xié)程。

綜上,如果從根本上把yield視為控制流程的方式,這樣就好理解協(xié)程了

協(xié)程可以認(rèn)為是可以明確標(biāo)記有某種語(yǔ)法元素(yield from)的階段“暫停”函數(shù)

生成器如何進(jìn)化為協(xié)程

協(xié)程的框架是在Python2.5(2006年)實(shí)現(xiàn)的。自此之后,yield關(guān)鍵字可以在表達(dá)式中使用,并且生成器增加了.send(value)方法。生成器的調(diào)用方法可以使用.send()發(fā)送數(shù)據(jù),發(fā)送的數(shù)據(jù)會(huì)成為生成器中yield表達(dá)式的值。因此生成器可以作為協(xié)程使用。協(xié)程是指一個(gè)過程,這個(gè)過程與調(diào)用方協(xié)作,產(chǎn)出調(diào)用方法提供的值。

除了.send()方法,后續(xù)還增加了.throw()和.close()方法:前者作用是讓調(diào)用方拋出異常,在生成器中處理;后者作用是終止生成器。

用作協(xié)程的生成器的基本行為

示例,一個(gè)簡(jiǎn)單的協(xié)程

def simple_coroutine():
    print('-> coroutine started')
    x = yield
    print('-> coroutine received:', x)
my_coro = simple_coroutine()
print(my_coro)  # 得到的是生成器對(duì)象
print(next(my_coro))  # None
print(my_coro.send(42))
打印
<generator object simple_coroutine at 0x00D957B0>
-> coroutine started
None
-> coroutine received: 42
Traceback (most recent call last):
  File "C:/Users/lijiachang/PycharmProjects/collect_demo/test2.py", line 13, in <module>
    print(my_coro.send(42))
StopIteration

知識(shí)點(diǎn):

  • 定義體中x = yield 這種形式,如果協(xié)程只需要從客戶那接收數(shù)據(jù),那么產(chǎn)出值是None,所以next(my_coro)的值為None,這個(gè)值是隱式指定的,因?yàn)閥ield關(guān)鍵字右邊沒有表達(dá)式。
  • 首先要調(diào)用next(my_coro),因?yàn)樯善鬟€沒有啟動(dòng),沒在yield語(yǔ)句處暫停,所以一開始是無(wú)法發(fā)送數(shù)據(jù)的,首先要調(diào)用next(),到y(tǒng)ield關(guān)鍵字處暫停。
  • 當(dāng)執(zhí)行到my_coro.send(42),協(xié)程定義體中的yield表達(dá)式會(huì)得到42,然后賦值給x;協(xié)程繼續(xù)運(yùn)行到下一個(gè)yield表達(dá)式,或者終止。
  • 最后,控制權(quán)流動(dòng)到協(xié)程定義體的末尾,導(dǎo)致生成器像往常一樣拋出StopIteration異常

協(xié)程的四個(gè)狀態(tài)

協(xié)程可以身處四個(gè)狀態(tài)中的一個(gè)。獲取當(dāng)前狀態(tài)可以使用inspect.getgeneratorstate()函數(shù)確定,該函數(shù)會(huì)返回下面四種狀態(tài)的一個(gè):

  • 'GEN_CREATED' :等待開始執(zhí)行
  • 'GEN_RUNNING' :解釋器正在執(zhí)行 (只有在多線程程序才能看到這個(gè)狀態(tài))
  • 'GEN_SUSPENDED' :在yield表達(dá)式處暫停
  • 'GEN_CLOSED' : 執(zhí)行結(jié)束

因?yàn)閟end方法的參數(shù)會(huì)成為暫停的yield表達(dá)式的值,所以僅當(dāng)協(xié)程處于暫停狀態(tài)時(shí)才能調(diào)用send方法,例如my_coro.send(42).。

不過,如果協(xié)程還沒激活(即狀態(tài)為GEN_CREATED),始終要調(diào)用next(my_coro)激活協(xié)程----也可以調(diào)用my_coro.send(None)激活。

ps:如果創(chuàng)建協(xié)程對(duì)象后立即把None之外的值發(fā)給它,會(huì)出現(xiàn)以下錯(cuò)誤,清晰明了:TypeError: can't send non-None value to a just-started generator

最先調(diào)用next(my_coro)函數(shù)這一步通常稱為“預(yù)激”(prime)協(xié)程,即讓協(xié)程向前執(zhí)行到第一個(gè)yield表達(dá)式,準(zhǔn)備好作為活躍的協(xié)程使用。

from inspect import getgeneratorstate
def simple_coro2(a):
    print('-> started: a=', a)
    b = yield a
    print('->  received: b=', b)
    c = yield a + b
    print('->  received: c=', c)
my_coro2 = simple_coro2(14)
print(getgeneratorstate(my_coro2))
print(next(my_coro2))
print(getgeneratorstate(my_coro2))
print(my_coro2.send(28))
print(my_coro2.send(99))
打印
GEN_CREATED
-> started: a= 14
14
GEN_SUSPENDED
->  received: b= 28
42
->  received: c= 99
Traceback (most recent call last):
  File "C:/Users/lijiachang/PycharmProjects/collect_demo/test2.py", line 19, in <module>
    print(my_coro2.send(99))
StopIteration

知識(shí)點(diǎn):

  • 協(xié)程在yield關(guān)鍵字所在的位置暫停執(zhí)行。
  • 在賦值語(yǔ)句中,=右邊的代碼在賦值前執(zhí)行。因此對(duì)于b = yield a這行代碼,等到客戶端代碼再激活協(xié)程時(shí)才會(huì)設(shè)定b的值。

示例-使用協(xié)程計(jì)算平均值

示例,一個(gè)累積計(jì)算平均值的函數(shù),使用協(xié)程實(shí)現(xiàn)

def averager():
    total = 0.0
    count = 0
    average = 0
    while True:
        num = yield average
        total += num
        count += 1
        average = total / count
aver = averager()
print(aver.send(None))  # 預(yù)激協(xié)程 
print(aver.send(2))
print(aver.send(4))
print(aver.send(6))

打印
0
2.0
3.0
4.0

知識(shí)點(diǎn):

  • 使用協(xié)程之前,需要預(yù)激協(xié)程,除了使用aver.send(None)還可以使用next(aver)
  • 這個(gè)示例中,是個(gè)無(wú)限循環(huán)的,僅當(dāng)調(diào)用方在協(xié)程上調(diào)用.close()方法,或者沒有對(duì)協(xié)程的引用而被垃圾回收程序回收時(shí),這個(gè)協(xié)程才會(huì)終止。

預(yù)激協(xié)程的裝飾器

如果不預(yù)激,那么協(xié)程沒什么用。

調(diào)用my_coro.send(x)之前,一定要先調(diào)用next(my_coro)。為了簡(jiǎn)化協(xié)程的用法,有時(shí)候會(huì)使用一個(gè)預(yù)激裝飾器。

from inspect import getgeneratorstate
from functools import wraps
def coroutine(func):
    """預(yù)激協(xié)程的裝飾器,向前執(zhí)行到第一個(gè)yield表達(dá)式"""
    @wraps(func)
    def primer(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)
        return gen
    return primer
@coroutine
def averager():
    total = 0.0
    count = 0
    average = 0
    while True:
        num = yield average
        total += num
        count += 1
        average = total / count
aver = averager()
print(getgeneratorstate(aver))
print(aver.send(2))
print(aver.send(4))
print(aver.send(6))

打印
GEN_SUSPENDED
2.0
3.0
4.0

可以看到,使用預(yù)激裝飾器裝飾了averager函數(shù)上之后,協(xié)助立即處于GEN_SUSPENED狀態(tài),因此這個(gè)協(xié)程已經(jīng)準(zhǔn)備好,可以接收值了。

很多框架都提供了處理協(xié)程的特殊裝飾器,不過不是所有裝飾器都用于預(yù)激協(xié)程,有些會(huì)提供其他服務(wù),例如勾入事件循環(huán)。

使用yeild from句法調(diào)用協(xié)程時(shí),會(huì)自動(dòng)預(yù)激,因此使用@coroutine等裝飾器不兼容。Python3.4標(biāo)準(zhǔn)庫(kù)中的asyncio.coroutine裝飾器不會(huì)預(yù)激協(xié)程,因此能兼容yeild from句法。

終止協(xié)程和異常處理

協(xié)程中未處理的異常會(huì)向上冒泡,傳給觸發(fā)協(xié)程的對(duì)象(next函數(shù)或send函數(shù)的調(diào)用方)。

未處理的異常會(huì)導(dǎo)致協(xié)程終止:

In [4]: aver.send(20)

Out[4]: 20.0

In [5]: aver.send(30)

Out[5]: 25.0

In [6]: aver.send('spam')

---------------------------------------------------------------------------

TypeError Traceback (most recent call last)

TypeError: unsupported operand type(s) for +=: 'float' and 'str'

In [7]: aver.send(40)

---------------------------------------------------------------------------

StopIteration Traceback (most recent call last)

StopIteration:

以上可以聯(lián)想到,終止協(xié)程的一種方式:發(fā)送某個(gè)哨符值,讓協(xié)程退出。

比如內(nèi)置的None和Ellipsis(省略號(hào)...)常量經(jīng)常用作哨符值。Ellipsis的優(yōu)點(diǎn)是數(shù)據(jù)流中不太常有這個(gè)值。也有人把StopIteration作為哨符值,這樣:my_coro.send(StopIteration)

從Python2.5開始,可以在生成器對(duì)象調(diào)用兩個(gè)方法,顯式的把異常發(fā)給協(xié)程。

這兩個(gè)方法是throw拋和close:

generator.throw(exc_type[, exc_value[, traceback]])

使生成器在yeild表達(dá)式處拋出指定的異常。

如果生成器處理了拋出的異常,代碼會(huì)向前執(zhí)行到下一個(gè)yeild表達(dá)式處,而產(chǎn)生的值會(huì)成為調(diào)用generator.throw方法得到的返回值。

如果生成器沒有處理拋出的異常,異常會(huì)向上冒泡,傳給調(diào)用方的上下文中。

generator.close()

使生成器在暫停的yield表達(dá)式處拋出GeneratorExit異常。

如果生成器沒有處理這個(gè)異常,或者拋出了StopIteration異常(表示運(yùn)行到結(jié)尾),調(diào)用方不會(huì)報(bào)錯(cuò)。

如果收到GeneratorExit異常,生成器一定不能產(chǎn)出值,否則解釋器會(huì)拋出RuntimeError異常。

生成器拋出的其他異常會(huì)向上冒泡,傳給調(diào)用方。

示例,使用close和throw方法控制協(xié)程。

from inspect import getgeneratorstate
class DemoException(Exception):
    pass
def demo_exc_handling():
    """異常處理demo"""
    print('-> coroutine started')
    while True:
        try:
            x = yield
        except DemoException:
            print('*** DemoException handled')  # 處理異常
        else:
            print('-> coroutine received: {!r}'.format(x))  # 如果沒有異常,顯示接收的值
exc_coro = demo_exc_handling()
next(exc_coro)
exc_coro.send(11)
exc_coro.close()  # 正常關(guān)閉協(xié)程
print(getgeneratorstate(exc_coro))
exc_coro2 = demo_exc_handling()
next(exc_coro2)
exc_coro2.throw(DemoException)  # 把異常傳入?yún)f(xié)程后,如果有處理,不會(huì)終止協(xié)程
exc_coro2.send(22)
print(getgeneratorstate(exc_coro2))
exc_coro2.throw(ZeroDivisionError)  # 如果把無(wú)法處理的異常傳入,協(xié)程會(huì)終止
打印
-> coroutine started
-> coroutine received: 11
GEN_CLOSED
-> coroutine started
*** DemoException handled
-> coroutine received: 22
GEN_SUSPENDED
Traceback (most recent call last):
  File "C:/Users/lijiachang/PycharmProjects/collect_demo/test2.py", line 33, in <module>
    exc_coro2.throw(ZeroDivisionError)
  File "C:/Users/lijiachang/PycharmProjects/collect_demo/test2.py", line 13, in demo_exc_handling
    x = yield
ZeroDivisionError

知識(shí)點(diǎn):

  • 使用.close()關(guān)閉協(xié)程,沒有任何異常拋出
  • 如果協(xié)程中有對(duì)應(yīng)的異常處理代碼,.throw()的異常不會(huì)終止協(xié)程。如果沒有異常處理,協(xié)程會(huì)終止

如果不管協(xié)程如何結(jié)束都想要做些清理工作的話,要把協(xié)程定義體匯總相關(guān)的代碼放入try/finally塊中

def demo_exc_handling():
    """異常處理demo"""
    print('-> coroutine started')
    try:
        while True:
            try:
                x = yield
            except DemoException:
                print('*** DemoException handled')  # 處理異常
            else:
                print('-> coroutine received: {!r}'.format(x))  # 如果沒有異常,顯示接收的值
    finally:
        print('-> coroutine ending.')
        print('do something.')

獲取協(xié)程返回值

下面是averager累積求平均數(shù)的另一個(gè)版本,這個(gè)版本不會(huì)隨著增加元素返回平均值,而是最后返回一個(gè)值。

from collections import namedtuple
from functools import wraps
def coroutine(func):
    """預(yù)激協(xié)程裝飾器"""
    @wraps(func)
    def prime(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)
        return gen
    return prime
Result = namedtuple('Result', 'count average')
@coroutine
def averager():
    count = 0
    average = 0.0
    total = 0
    while True:
        item = yield
        if item is None:
            break  # 為了獲取返回值,協(xié)程必須正常終止,因此要有個(gè)判斷,以便退出累計(jì)循環(huán)
        count += 1
        total += item
        average = total / count
    return Result(count, average)  # 最終返回一個(gè)namedtuple,包含信息
ave = averager()
ave.send(2)
ave.send(4)
print(ave.send(None))
打印
Traceback (most recent call last):
  File "C:/Users/lijiachang/PycharmProjects/collect_demo/test2.py", line 39, in <module>
    print(ave.send(None))
StopIteration: Result(count=2, average=3.0)

知識(shí)點(diǎn):

  • 使用ave.send(None)發(fā)送None終止循環(huán),或者使用next(ave)也可以。結(jié)果就是導(dǎo)致協(xié)程結(jié)束,返回結(jié)果
  • 代碼中break跳出了while循環(huán),導(dǎo)致運(yùn)行到定義體結(jié)束,也就拋出StopIteration很正常。
  • 異常對(duì)象的value保存著return返回的值。return表達(dá)式的值會(huì)偷偷傳給調(diào)用方,賦值給StopIteration異常的一個(gè)屬性。這樣做有點(diǎn)不合常理,但是能保留生成器對(duì)象的常規(guī)行為:耗盡時(shí)拋出StopIteration異常。

既然協(xié)程定義體中return返回值,是寄托到了異常的value值中,那么就捕獲異常:

try:
    print(ave.send(None))
except StopIteration as e:
    result = e.value
print(result)
打印
Result(count=2, average=3.0)

獲取協(xié)程的返回值雖然要繞個(gè)圈子,但是這是PEP 380定義的方式。

但是我們平時(shí)不需要這樣做,因?yàn)閥ield from結(jié)構(gòu)會(huì)在內(nèi)部自動(dòng)捕獲StopIteration異常。這種處理方式和for循環(huán)處理StopIteration異常的方式一樣:循環(huán)機(jī)制會(huì)讓給用戶易于理解的方式處理異常。

而且,對(duì)yeild from結(jié)構(gòu)來(lái)說,不僅可以捕獲StopIteration異常,還會(huì)把異常的value屬性值變?yōu)閥eild from的值。

所以接下來(lái)要介紹yield from結(jié)構(gòu)

使用yield from

要知道yield from是全新的語(yǔ)言結(jié)構(gòu)。它的作用比yield多得多,因此人們認(rèn)為繼續(xù)使用yield 關(guān)鍵字多少會(huì)引起誤解。

在Python3.4之后,被await關(guān)鍵字代替。

在生成器gen中使用yeild from subgen()時(shí),subgen會(huì)獲得控制權(quán),把產(chǎn)出的值傳給gen的調(diào)用方,即調(diào)用方直接可以控制subgen來(lái)得到產(chǎn)出值。以此同時(shí),gen會(huì)阻塞,帶燈subgen終止。

yield from x表達(dá)式對(duì)x對(duì)象所做的第一件事是,調(diào)用iter(x),從中獲取迭代器。因此x是任何可迭代對(duì)象。

yield from 結(jié)構(gòu)不是簡(jiǎn)單的替代產(chǎn)出值的嵌套for循環(huán),而是把職責(zé)委托給子生成器的句法。

yield from 的主要功能是打開雙向通道,把最外層的調(diào)用方和最內(nèi)層的子生成器連接起來(lái),

這樣二者可以直接發(fā)送和產(chǎn)出值,還可以直接傳入異常,而不用在位于中間的協(xié)程中添加大量打異常處理。有了這個(gè)結(jié)構(gòu),協(xié)程可以通過以前不可能的方式委托職責(zé)。

相關(guān)的專業(yè)術(shù)語(yǔ):

委派生成器

包含yield from <iterable> 表達(dá)式的生成器函數(shù)。

子生成器

從yield from表達(dá)式中<iterable>部分獲取的生成器。

調(diào)用方

指代調(diào)用委派生成器的客戶端代碼。

委派生成器在yield from表達(dá)式處暫停時(shí),調(diào)用方可以直接把數(shù)據(jù)發(fā)給子生成器,子生成器再把產(chǎn)生的值發(fā)給調(diào)用方。子生成器返回之后,解釋器會(huì)拋出StopIteration異常,并把返回值附加到異常對(duì)象上(異常的value屬性),此時(shí)委派生成器會(huì)恢復(fù)。

下面的示例,用于說明yield from結(jié)構(gòu)的用法

from collections import namedtuple
from functools import wraps
Result = namedtuple('Result', 'count average')
# 作為子生成器使用
def averager():
    count = 0
    average = 0.0
    total = 0
    while True:
        item = yield
        if item is None:
            break
        count += 1
        total += item
        average = total / count
    return Result(count, average)  # 返回的結(jié)果最后會(huì)成為grouper函數(shù)中的yield from中的值
# 委派生成器
def grouper(results, key):
    while True:  # 每次循環(huán)都會(huì)產(chǎn)生一個(gè)新的averager實(shí)例,每個(gè)實(shí)例都作為協(xié)程使用的生成器對(duì)象
        results[key] = yield from averager()  # grouper每次接受的到值都通過yield from處理,通過管道傳給averager實(shí)例
# 客戶端代碼,即調(diào)用方
def main(data):
    results = {}
    for key, values in data.items():
        group = grouper(results, key)  # group作為協(xié)程使用
        next(group)  # 預(yù)激協(xié)程
        for value in values:
            group.send(value)  # 把value傳給grouper,最終到達(dá)的是averager函數(shù)的item = yield那行。
        group.send(None)  # 把None傳給grouper,讓當(dāng)前averager實(shí)例終止,讓grouper繼續(xù)運(yùn)行。
    print(results)
data = {
    'girls;kg': [40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
    'girls;m': [1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
    'boys;kg': [39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
    'boys;m': [1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
}
if __name__ == "__main__":
    main(data)

知識(shí)點(diǎn):

  • grouper會(huì)在yield from表達(dá)式處暫停,等待averager實(shí)例處理客戶端發(fā)來(lái)的值。averager實(shí)例運(yùn)行完畢后,返回的值綁定到了results[key]上,while循環(huán)會(huì)不斷創(chuàng)建averager實(shí)例,處理更多的值
  • 對(duì)于委派生成器grouper來(lái)說,永遠(yuǎn)不知道傳入的值是什么。因?yàn)楫?dāng)value值傳給grouper,最終到達(dá)的是averager函數(shù)的item = yield那行。

group.send(None) 的作用非常重要,它讓當(dāng)前的averager實(shí)例終止,然后在創(chuàng)建一個(gè)新的averager實(shí)例。如果沒有它,results結(jié)果中不會(huì)有任何內(nèi)容。

下面說明沒有g(shù)roup.send(None) 時(shí),最終的運(yùn)作方式:

  • 外層for循環(huán)每次迭代會(huì)新建一個(gè)grouper實(shí)例,賦值給group變量;group是委派生成器
  • 調(diào)用next(group),是預(yù)激委派生成器grouper,此時(shí)進(jìn)入while Ture循環(huán),調(diào)用子生成器averager后,在yield from表達(dá)式處暫停
  • 內(nèi)層for循環(huán)調(diào)用group.send(value),直接把值傳給子生成器averager。同時(shí),當(dāng)前的grouper實(shí)例(group)在yield from表達(dá)式處暫停。
  • 內(nèi)層for循環(huán)結(jié)束后,group實(shí)例依舊在yield from表達(dá)式處暫停,因此,grouper函數(shù)定義體中為results[key]賦值的語(yǔ)句還沒有執(zhí)行。
  • 如果外層for循環(huán)的末尾沒有g(shù)roup.send(None),那么averager子生成器永遠(yuǎn)不會(huì)終止,委派生成器group永遠(yuǎn)不會(huì)再次激活,因此永遠(yuǎn)不會(huì)為results[key]賦值
  • 外層for循環(huán)重新迭代時(shí)會(huì)新建一個(gè)grouper實(shí)例,然后綁定到group變量上。前一個(gè)grouper實(shí)例以及它創(chuàng)建的未終止的averager子生成器實(shí)例,會(huì)被垃圾回收程序回收。

上面想說明的關(guān)鍵一點(diǎn)是,如果子生成器不終止,委派生成器會(huì)在yield from表達(dá)式處永遠(yuǎn)暫停。

如果這樣,程序不會(huì)向前進(jìn)行,因?yàn)閥ield from把控制權(quán)交給了客戶端代碼(即委派生成器的調(diào)用方)。

以上的示例和說明,展示了yield from結(jié)構(gòu)最簡(jiǎn)單的用法,只有委派生成器和一個(gè)子生成器。因?yàn)槲缮善飨喈?dāng)于管道,所以可以把任意數(shù)量個(gè)委派生成器連接在一起:一個(gè)委派生成器使用yield from調(diào)用一個(gè)子生成器,而那個(gè)子生成器本身也是委派生成器,使用yield from調(diào)用另一個(gè)生成器,以此類推。最終,這個(gè)鏈條要以一個(gè)只使用yield 表達(dá)式的簡(jiǎn)單生成器結(jié)束,或者以任何可迭代對(duì)象結(jié)束。

任何yield from鏈條都必須由客戶驅(qū)動(dòng),在最外層委派生成器上調(diào)用next()函數(shù)或.send()方法。可以用for循環(huán)等隱式調(diào)用。

yield from的意義

在PEP 380中有這么一段話,yield from的作者Greg Ewing的草稿,可以粗略的解釋:

把迭代器當(dāng)作生成器使用,相當(dāng)于 把子生成器的定義體內(nèi)聯(lián)在yield from表達(dá)式中。此外,子生成器可以執(zhí)行return語(yǔ)句,返回一個(gè)值,而返回的值會(huì)成為yield from表達(dá)式的值。

批準(zhǔn)后的PEP 380,分六點(diǎn)說明了yield from的行為:

  • 子生成器產(chǎn)出的值都直接傳給委派生成器的調(diào)用方(即客戶端代碼)
  • 使用send()方法發(fā)送給委派生成器的值都直接傳給子生成器。如果發(fā)送的值是None,那么會(huì)調(diào)用子生成器的__next__()方法。如果發(fā)送的值不是None,那么會(huì)調(diào)用子生成器的send()方法。如果調(diào)用的方法拋出StopIteration異常,那么委派生成器會(huì)恢復(fù)運(yùn)行。任何的其他異常會(huì)向上冒泡,傳給委派生成器。
  • 生成器退出時(shí),生成器或者子生成器中的return expr表達(dá)式會(huì)觸發(fā)StopIteration(expr)異常拋出。
  • yield from表達(dá)式的值,是子生成器終止時(shí)傳給StopIteration異常的第一個(gè)參數(shù)。
  • 傳入委派生成器的異常,除了GeneratorExit之外都傳給子生成器的throw()方法。如果調(diào)用throw()方法時(shí)拋出StopIteration異常,委派生成器恢復(fù)運(yùn)行。StopIteration之外的異常會(huì)向上冒泡,傳給委派生成器。
  • 如果把GeneratorExit異常傳給委派生成器,或者在委派生成器上調(diào)用close()方法,那么會(huì)在子生成器上調(diào)用close()方法(前提是有此方法)。如果調(diào)用close()方法導(dǎo)致異常拋出,那么異常會(huì)向上冒泡,傳給委派生成器;否則,委派生成器拋出GeneratorExit異常。

yield from的具體語(yǔ)義難以理解,尤其是最后兩點(diǎn)。

Greg Ewing還使用了偽代碼,演示了yield from的行為。下面是把原40行的偽代碼簡(jiǎn)化了的邏輯,因?yàn)樵?0行代碼難以理解。假設(shè)了客戶端代碼有在委派生成器上調(diào)用.throw()和.close()方法,假設(shè)子生成器不會(huì)拋出異常,而是一直運(yùn)行到終止,讓解釋器拋出StopIteration異常。

下面列出的偽代碼,是對(duì)這行代碼的擴(kuò)充:RESULT = yield from EXPR

"""RESULT = yield from EXPR 的偽代碼"""
_i = iter(EXPR)  # EXPR 為任何可迭代對(duì)象,獲取迭代器_i使用iter()函數(shù),這里是獲取子生成器
try:
    _y = next(_i)  # 預(yù)激子生成器,把結(jié)果保存在_y中,作為產(chǎn)出的第一個(gè)值
except StopIteration as _e:
    _r = _e.value  # 如果拋出StopIteration異常,獲取異常對(duì)象中的value屬性,賦值給_r 這是最簡(jiǎn)單情況下的返回值
else:
    while 1: # 運(yùn)行這個(gè)循環(huán)時(shí),委派生成器會(huì)阻塞,值作為調(diào)用方和子生成器之間的通道
        _s = yield _y  # 產(chǎn)出子生成器當(dāng)前產(chǎn)出的元素;等待調(diào)用方發(fā)送_s保存的值。
        try:
            _y = _i.send(_s)  # 嘗試讓子生成器向前執(zhí)行,轉(zhuǎn)發(fā)調(diào)用方發(fā)送的_s
        except StopIteration as _e: # 如果子生成器拋出StopIteration異常,獲取異常對(duì)象中的value屬性,賦值給_r;然后退出循環(huán),委派生成器恢復(fù)運(yùn)行
            _r = _e.value
            break
RESULT = _r # 返回的結(jié)果是_r,即是整個(gè)yield from表達(dá)式的值

RESULT = _r # 返回的結(jié)果是_r,即是整個(gè)yield from表達(dá)式的值

  • _i : 迭代器,子生成器
  • _y : 產(chǎn)出的值,子生成器產(chǎn)出的值
  • _r : 結(jié)果,最終的結(jié)果,即子生成器運(yùn)行結(jié)束后yield from表達(dá)式的值
  • _s : 發(fā)送的值,調(diào)用方發(fā)給委派生成器的值,這個(gè)值會(huì)轉(zhuǎn)發(fā)給子生成器
  • _e : 異常對(duì)象

但是,現(xiàn)實(shí)情況會(huì)復(fù)雜一些,因?yàn)榭蛻粢獙?duì).throw()和.close()方法調(diào)用,二者兩個(gè)方法的執(zhí)行操作,必須傳入子生成器。

子生成器可能只是純粹的迭代器,不支持.throw和.close()方法,因此yield from結(jié)構(gòu)邏輯必須處理這種情況。

如果子生成器實(shí)現(xiàn)了這兩個(gè)方法,而在子生成器內(nèi)部,這兩個(gè)方法都會(huì)觸發(fā)異常拋出,這種情況也必須由yield from機(jī)制處理。

調(diào)用方可能會(huì)無(wú)緣無(wú)故的讓子生成器自己拋出異常,實(shí)現(xiàn)yield from結(jié)構(gòu)時(shí)要處理這種情況。

最后,為了優(yōu)化,如果調(diào)用方調(diào)用next函數(shù)或者send方法,都要轉(zhuǎn)交職責(zé),在子生成器上調(diào)用next函數(shù),僅當(dāng)調(diào)用方發(fā)送的值不是None時(shí),才使用子生成器的send方法。

完整的偽代碼,參見《流暢的Python》第401頁(yè)

使用協(xié)程做離散事件仿真

協(xié)程能自然地表述很多算法,例如仿真、游戲、異步IO,以及其他事件驅(qū)動(dòng)型變成形式或協(xié)作式多任務(wù)。--Guido

協(xié)程是asyncio包的基礎(chǔ)構(gòu)。通過仿真系統(tǒng)能夠說明如何使用協(xié)程代替線程實(shí)現(xiàn)并發(fā)活動(dòng)。

離散事件仿真(DES:Discrete Event Simulation)是一種把系統(tǒng)建模成一系列事件的仿真類型。

在離散事件仿真中,仿真“鐘”向前推進(jìn)的量不是固定的,而是直到推進(jìn)到下一個(gè)事件模型的模擬時(shí)間。假如我們抽象模擬出租車的運(yùn)營(yíng)過程,其中一個(gè)事件是乘客上車,下一個(gè)事件則是乘客下車。使用離散事件仿真可以在不到一秒鐘的時(shí)間內(nèi)模擬一年的出租車運(yùn)營(yíng)過程。這與連續(xù)仿真不同,連續(xù)仿真的仿真鐘以固定的量不斷向前前進(jìn)。

顯然,回合制游戲就是離散事件仿真的例子:游戲的狀態(tài)只在玩家操作時(shí)變化,而且一旦玩家決定下一步怎么走了,仿真鐘就會(huì)凍結(jié)。而實(shí)時(shí)游戲則是連續(xù)仿真,仿真鐘一直在運(yùn)行,游戲的狀態(tài)在一秒鐘內(nèi)更新很多次。

這兩種仿真類型都能使用多線程或在單線程中使用面向事件的編程技術(shù)(例如事件循環(huán)驅(qū)動(dòng)的回調(diào)或協(xié)程)實(shí)現(xiàn)。可以說,為了實(shí)現(xiàn)連續(xù)仿真,在多個(gè)線程中處理實(shí)時(shí)并行的操作更自然。而協(xié)程恰好為實(shí)現(xiàn)離散事件仿真提供了合理的抽象。SimPy是一個(gè)實(shí)現(xiàn)離散事件仿真的Python包,通過一個(gè)協(xié)程表示離散事件仿真系統(tǒng)的各個(gè)進(jìn)程。

到此這篇關(guān)于Python協(xié)程原理全面分析的文章就介紹到這了,更多相關(guān)Python協(xié)程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論