什么是協(xié)程

協(xié)程

to yield 含義:產(chǎn)出和讓步瘩扼。

yield item這行代碼會產(chǎn)出一個值,提供給next(...)的調(diào)用方垃僚;此外集绰,還會作出讓步,暫停執(zhí)行生成器谆棺,讓調(diào)用方繼續(xù)工作栽燕,直到需要使用另一個值時再調(diào)用next()。調(diào)用方會從生成器中拉取值改淑。

從根本上把yield視作控制流程的方式碍岔,這樣就好理解協(xié)程了。

將生成器當作一個協(xié)程

生成器的調(diào)用方可以使用.send(...)方法發(fā)送數(shù)據(jù)朵夏,發(fā)送的數(shù)據(jù)會成為生成器函數(shù)中yield表達式的值蔼啦。因此,生成器可以作為協(xié)程使用仰猖。協(xié)程是指一個過程捏肢,這個過程與調(diào)用方協(xié)作,產(chǎn)出由調(diào)用方提供的值饥侵。

def simple_coroutine():
    print("->coroutine started")
    x = yield # ①
    print("-> coroutine received:", x)

my_coro = simple_coroutine()
next(my_coro)  # ②
my_coro.send(42) # ③

# 輸出:
->coroutine started
Traceback (most recent call last):
-> coroutine received: 42
  File "C:/Users/42072/PycharmProjects/day01/ready04/yield_reday.py", line 62, in <module>
    my_coro.send(42)
StopIteration
  • ① yield在表達式中使用鸵赫;如果協(xié)程只需從客戶那里接收數(shù)據(jù),那么產(chǎn)出的值是None——這個值是隱式指定的躏升,因為yield關(guān)鍵字右邊沒有表達式辩棒。
  • ② 首先要調(diào)用next(...)函數(shù),因為生成器還沒啟動,沒在yield語句處暫停一睁,所以一開始無法發(fā)送數(shù)據(jù)藕赞。
  • ③ 調(diào)用這個方法后,協(xié)程定義體中的yield表達式會計算出42卖局;現(xiàn)在斧蜕,協(xié)程會恢復,一直運行到下一個yield表達式砚偶,或者終止批销。

協(xié)程的狀態(tài)

  1. GEN_CREATED:等待開始執(zhí)行
  2. GEN_RUNNING:解釋器正在執(zhí)行
  3. GEN_SUSPENDED:在yield表達式處暫停
  4. GEN_CLOSED:執(zhí)行結(jié)束
from inspect import getgeneratorstate

def simple_coroutine():
    print("->coroutine started")
    x = yield
    print(getgeneratorstate(my_coro))
    print("-> coroutine received:", x)

my_coro = simple_coroutine()
print(getgeneratorstate(my_coro))
next(my_coro)
print(getgeneratorstate(my_coro))
my_coro.send(42)
print(getgeneratorstate(my_coro))

注意:

  • 因為send方法的參數(shù)會成為暫停的yield表達式的值,所以染坯,僅當協(xié)程處于暫停狀態(tài)時才能調(diào)用send方法均芽。
  • 如果給未激活的協(xié)程對象發(fā)送None以外的值,會引發(fā)錯誤单鹿。

激活協(xié)程的方式有兩種:

  • next(my_coro)方法
  • my_coro.send(None)

示例:協(xié)程產(chǎn)出兩個值

def simple_coro2(a):
    print('->Started :a = ' ,a)
    b = yield a
    print('->Started :b = ' ,b)
    c = yield a + b
    print('->Received: c=',c)

my_coro2 = simple_coro2(14)
next(my_coro2)
rs1 = my_coro2.send(28)
print(rs1)
rs2 = my_coro2.send(99)
print(rs2)

案例:計算移動平均值

def averager():
    total = 0.0
    count = 0
    average = None

    while True:
        term = yield average
        total += term
        count += 1
        average = total / count

coro_avg = averager()
next(coro_avg)
print(coro_avg.send(10))
print(coro_avg.send(20))
print(coro_avg.send(30))
#輸出:
10.0
15.0
20.0

使用協(xié)程之前必須預激掀宋,可是這一步容易忘記。為了避免忘記仲锄,可以在協(xié)程上使用一個特殊的裝飾器劲妙。

  • 額外知識:wraps
作者:hqzxsc2006 
來源:CSDN 
原文:https://blog.csdn.net/hqzxsc2006/article/details/50337865 

Python裝飾器(decorator)在實現(xiàn)的時候,被裝飾后的函數(shù)其實已經(jīng)是另外一個函數(shù)了(函數(shù)名等函數(shù)屬性會發(fā)生改變)儒喊,為了不影響镣奋,Python的functools包中提供了一個叫wraps的decorator來消除這樣的副作用。寫一個decorator的時候怀愧,最好在實現(xiàn)之前加上functools的wrap侨颈,它能保留原有函數(shù)的名稱和docstring。

#不加wraps
def my_decorator(func):
    
    def wrapper(*args, **kwargs):
        """decorator"""
        print("Calling decorated function...")
        return func(*args,**kwargs)

    return wrapper

@my_decorator
def example():
    """Docstring"""
    print('Called example function')
    
print(example.__name__,example.__doc__)

#輸出:
wrapper decorator


#加wraps

from functools import wraps
def my_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        """decorator"""
        print("Calling decorated function...")
        return func(*args,**kwargs)

    return wrapper

@my_decorator
def example():
    """Docstring"""
    print('Called example function')
    
print(example.__name__,example.__doc__)

#輸出:
example Docstring

預激協(xié)程

協(xié)程如果不激活芯义,那么則沒什么用哈垢。調(diào)用send()方法之前,需要先調(diào)用next(my_coro)方法進行激活扛拨。為了簡化協(xié)程的用法耘分,有時會使用一個預激裝飾器。

from functools import wraps


def coroutine(func):
    @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 = None

    while True:
        term = yield average
        total += term
        count += 1
        average = total / count

from inspect import getgeneratorstate
coro_avg = averager()
print(getgeneratorstate(coro_avg))
print(coro_avg.send(10))
print(coro_avg.send(30))

輸出:
GEN_SUSPENDED
10.0
20.0

如果使用yield from 語法鬼癣,會自動預激陶贼。asyncio.coroutine裝飾器不會預激活協(xié)程,因此能兼容yield from 句法待秃。

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

協(xié)程中未處理的異常會向上冒泡拜秧,傳給next函數(shù)或send方法的調(diào)用方。

例如:

coro_avg = averager()
print(coro_avg.send(20))
print(coro_avg.send('aa'))

  • 由于第三行發(fā)送的不是數(shù)據(jù)章郁,導致協(xié)程內(nèi)部拋出異常枉氮。
  • 由于協(xié)程內(nèi)部沒有處理異常志衍,協(xié)程會終止。如果試圖重新激活協(xié)程聊替,會拋出StopIteration異常楼肪。

python 2.5以后,客戶代碼可以調(diào)用以下兩個方法惹悄,顯示地把異常發(fā)給協(xié)程:

  • generator.throw(exec_type[,exc_value[,traceback]])

致使生成器在暫停的yield表達式處拋出指定的異常春叫。如果生成器處理了拋出的異常,代碼會向前執(zhí)行到下一個yield表達式泣港,而產(chǎn)出的值會成為調(diào)用generator.throw方法得到的返回值暂殖。如果生成器沒有處理拋出的異常,異常會向上冒泡当纱,傳到調(diào)用方的上下文中呛每。

  • generator.close()

致使生成器在暫停的yield表達式處拋出GeneratorExit異常。如果生成器沒有處理這個異常坡氯,或者拋出了StopIteration異常(通常是指運行到結(jié)尾)晨横,調(diào)用方不會報錯。如果收到GeneratorExit異常箫柳,生成器一定不能產(chǎn)出值手形,否則解釋器會拋出RuntimeError異常。生成器拋出的其他異常會向上冒泡滞时,傳給調(diào)用方叁幢。

示例:調(diào)用

class DemoException(Exception):
    """自定義異常"""

def demo_exc_handling():
    print("-> oroutine started")

    while True:
        try :
            x = yield
        except DemoException:
            print('*** DemoException handled. Continuing...')
        else :
            print('-> coroutine received: {!r}'.format(x))

    raise RuntimeError('永遠不執(zhí)行')

exc_coro = demo_exc_handling()
next(exc_coro)
exc_coro.send(11)
exc_coro.send(22)
exc_coro.close()
from inspect import getgeneratorstate
print(getgeneratorstate(exc_coro))

如果傳入 DemoException,則協(xié)程會正常處理,并繼續(xù)運行坪稽。

exc_coro = demo_exc_handling()
next(exc_coro)
exc_coro.send(11)
exc_coro.send(22)
# 將 DemoException 傳入
exc_coro.throw(DemoException)
from inspect import getgeneratorstate
print(getgeneratorstate(exc_coro))

如果傳入的異常沒有處理,協(xié)程會立即停止鳞骤,變成'GEN_CLOSED'狀態(tài)窒百。

exc_coro = demo_exc_handling()
next(exc_coro)
exc_coro.send(11)
exc_coro.send(22)
# 將 DemoException 傳入
exc_coro.throw(ZeroDivisionError)
from inspect import getgeneratorstate
print(getgeneratorstate(exc_coro))

如果協(xié)程必須做一些清理工作,則可以在協(xié)程體中放入 try/finally 代碼塊豫尽。

def demo_finally():
    print('-> coroutine started')
    try:
        while True:
            try:
                x = yield
            except DemoException:
                print('*** DemoException handled. Continuing...')
            else:
                print('-> coroutine received: {!r}'.format(x))
    finally:
        print("->coroutine ending...")

demo_coro = demo_finally();
next(demo_coro)
demo_coro.send(11)
demo_coro.send(ZeroDivisionError)

讓協(xié)程返回值

協(xié)程可以在執(zhí)行時不產(chǎn)出值篙梢,而是在最后返回一個值(通常是累計值)。

例子:一次返回平均值

from collections import namedtuple
Result = namedtuple('Result','count average')

def averager():
    total = 0.0
    count = 0
    average = None

    while True:
        term = yield
        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)
coro_avg.send(30)
coro_avg.send(20)
coro_avg.send(None)

#輸出:
Traceback (most recent call last):
  File "C:/Users/42072/PycharmProjects/day01/ready04/cor.py", line 109, in <module>
    coro_avg.send(None)
StopIteration: Result(count=3, average=20.0)

發(fā)送None會終止循環(huán)美旧,導致協(xié)程結(jié)束渤滞,返回結(jié)果。一如既往榴嗅,生成器對象會拋出StopIteration異常妄呕。異常對象的value屬性保存著返回的值。

coro_avg = averager();
next(coro_avg)
coro_avg.send(10)
coro_avg.send(30)
coro_avg.send(20)
try:
    coro_avg.send(None)
except StopIteration as exc:
    result = exc.value

print(result)

yield from結(jié)構(gòu)會在內(nèi)部自動捕獲StopIteration異常嗽测。對yield from結(jié)構(gòu)來說绪励,解釋器不僅會捕獲StopIteration異常肿孵,還會把value屬性的值變成yield from表達式的值。

yield from 句法

在生成器gen中使用yield from subgen()時疏魏,subgen會獲得控制權(quán)停做,把產(chǎn)出的值傳給gen的調(diào)用方,即調(diào)用方可以直接控制subgen大莫。與此同時蛉腌,gen會阻塞,等待subgen終止只厘。

用來簡化for循環(huán)中的yield表達式

def gen():
    for c in 'AB':
        yield c

    for i in range(1,3):
        yield i

print(list(gen()))

# 可以改寫為:  

def gen():
    yield from 'AB'
    yield from range(1,3)
    
print(list(gen()))

yield from x表達式對x對象所做的第一件事是烙丛,調(diào)用iter(x),從中獲取迭代器懈凹。因此蜀变,x可以是任何可迭代的對象。

把職責委托給子生成器

yield from 的主要功能是打開雙向通道介评,把最外層的調(diào)用方與最內(nèi)層的子生成器連接起來库北,這樣二者可以直接發(fā)送和產(chǎn)出值,還可以直接傳入異常们陆,而不用在位于中間的協(xié)程中添加大量處理異常的樣板代碼寒瓦。通過這個結(jié)構(gòu),協(xié)程可以把功能委托給子生成器坪仇。

主要術(shù)語:

  1. 委派生成器:
    包含 yield from <iterable> 表達式的生成器函數(shù)杂腰。

  2. 子生成器:
    從 yield from 表達式中 <iterable>部分獲取的生成器。

  3. 調(diào)用方
    調(diào)用委派生成器的客戶端代碼椅文。

graph LR
調(diào)用方-->委派生成器
委派生成器-->子生成器

示例:
計算7年級學生的體重和身高的平均值:

from collections import namedtuple

Result = namedtuple('Result','count average')

# 子生成器
def averager():
    total = 0.0
    count = 0
    average = None

    while True:
        # main 方法中發(fā)送的各種值喂很,會綁定到term變量上
        term = yield
        
        # 子生成器終止的條件
        if term is None:
            break

        total += term
        count += 1
        average = total / count
    
    # 返回值會成為grouper中 yield from表達式的值
    return Result(count,average)

# 委派生成器
def grouper(results,key):
    while True:
        # 每次迭代都會生成一個averager實例。每個生成器都是本協(xié)程(grouper)使用的生成器對象皆刺。
        results[key] = yield from averager()

# 客戶端代碼
def main(data):
    results = {}
    for key,values in data.items():
        # results 用來存儲結(jié)果
        group = grouper(results, key)
        # 預激活協(xié)程
        next(group)
        for value in values:
            # 發(fā)送的每個值都會經(jīng)由grouper的yield from處理少辣,通過管道傳給averager實例。同時羡蛾,當前的grouper實例漓帅,會在yield from 處暫停。
            group.send(value)
        # 把None值傳入grouper痴怨,導致當前的averager實例終止忙干,并讓grouper繼續(xù)運行,再創(chuàng)建一個aveager實例浪藻,處理下一組值捐迫。
        group.send(None)
    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]
}

main(data)

# 輸出:
{'girls;kg': Result(count=10, average=42.040000000000006), 'girls;m': Result(count=10, average=1.4279999999999997), 'boys;kg': Result(count=9, average=40.422222222222224), 'boys;m': Result(count=9, average=1.3888888888888888)}

委派生成器在yield from 表達式處暫停時,調(diào)用方可以直接把數(shù)據(jù)發(fā)給子生成器珠移,子生成器再把產(chǎn)出的值發(fā)給調(diào)用方弓乙。子生成器返回之后末融,解釋器會拋出StopIteration異常,并把返回值附加到異常對象上暇韧,此時委派生成器會恢復運行勾习。

注意:

  • 如果子生成器不終止,委派生成器會在yield from處永遠暫停懈玻。
  • 因為委派生成器相當于管道巧婶,所以可以把任意數(shù)量個委派生成器連接在一起:一個委派生成器使用yield from調(diào)用一個子生成器,而那個子生成器本身也是委派生成器涂乌,使用yield from調(diào)用另一個子生成器艺栈,以此類推。最終湾盒,這個鏈條要以一個只使用yield表達式的簡單生成器結(jié)束湿右;不過,也能以任何可迭代的對象結(jié)束罚勾。
  • 任何yield from鏈條都必須由客戶驅(qū)動毅人,在最外層委派生成器上調(diào)用next(...)函數(shù)或.send(...)方法。

yield from的意義

  1. 子生成器產(chǎn)出的值都直接傳給委派生成器的調(diào)用方(即客戶端代碼)尖殃。
  2. 使用send()方法發(fā)給委派生成器的值都直接傳給子生成器丈莺。如果發(fā)送的值是None,那么會調(diào)用子生成器的next()方法送丰。如果發(fā)送的值不是None缔俄,那么會調(diào)用子生成器的send()方法。如果調(diào)用的方法拋出StopIteration異常器躏,那么委派生成器恢復運行俐载。任何其他異常都會向上冒泡,傳給委派生成器登失。
  3. 生成器退出時瞎疼,生成器(或子生成器)中的return expr表達式會觸發(fā)StopIteration(expr)異常拋出。
  4. yield from表達式的值是子生成器終止時傳給StopIteration異常的第一個參數(shù)壁畸。
  5. 傳入委派生成器的異常,除了GeneratorExit之外都傳給子生成器的throw()方法茅茂。如果調(diào)用throw()方法時拋出StopIteration異常捏萍,委派生成器恢復運行。StopIteration之外的異常會向上冒泡空闲,傳給委派生成器令杈。
  6. 如果把GeneratorExit異常傳入委派生成器,或者在委派生成器上調(diào)用close()方法碴倾,那么在子生成器上調(diào)用close()方法逗噩,如果它有的話掉丽。如果調(diào)用close()方法導致異常拋出,那么異常會向上冒泡异雁,傳給委派生成器捶障;否則,委派生成器拋出GeneratorExit異常纲刀。

協(xié)程能自然地表述很多算法项炼,例如仿真、游戲示绊、異步I/O锭部,以及其他事件驅(qū)動型編程形式或協(xié)作式多任務。

案例:出租車運營仿真

import collections
# time 事件發(fā)生時間  proc:出租車編號  action:活動描述
Event = collections.namedtuple('Event','time proc action')

# 每輛出租車調(diào)用一次該函數(shù)面褐,用于創(chuàng)建一個生成器對象拌禾,用來表示各輛出租車的運營過程。
# ident是出租車編號
# trips 是出租車回家之前的形成數(shù)量
def taxi_process(ident,trips, start_time = 0):
    """每次改變狀態(tài)時創(chuàng)建事件展哭,把控制權(quán)讓給仿真器"""

    # 離開停車場事件湃窍,執(zhí)行到此,會暫停摄杂。當需要重新激活這個進程時坝咐,主循環(huán)會使用send方法發(fā)送當前的仿真事件賦值給time
    time = yield Event(start_time,ident,'leave garage')

    #每次行程都會執(zhí)行一遍此處的代碼塊
    for i in range(trips):
        # 產(chǎn)生一個Event實例,表示拉到乘客了析恢。協(xié)程在這里會暫停墨坚,需要激活時,主循環(huán)會使用send方法發(fā)送當前時間映挂。
        time = yield Event(time,ident,'pick up passenger')
        time = yield Event(time,ident,'drop off passenger')

    # 指定行程數(shù)量完成后泽篮,產(chǎn)生回家事件。此處柑船,協(xié)程最后一次暫停帽撑。
    yield Event(time,ident,'going home')


taxi = taxi_process(ident=12,trips=2,start_time=0)
next(taxi)
print(taxi.send( 7))
print(taxi.send(10))
print(taxi.send(15))
print(taxi.send(25))
print(taxi.send(35))

# 輸出:
Event(time=7, proc=12, action='pick up passenger')
Event(time=10, proc=12, action='drop off passenger')
Event(time=15, proc=12, action='pick up passenger')
Event(time=25, proc=12, action='drop off passenger')
Event(time=35, proc=12, action='going home')

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市鞍时,隨后出現(xiàn)的幾起案子亏拉,更是在濱河造成了極大的恐慌,老刑警劉巖逆巍,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件及塘,死亡現(xiàn)場離奇詭異,居然都是意外死亡锐极,警方通過查閱死者的電腦和手機笙僚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來灵再,“玉大人肋层,你說我怎么就攤上這事亿笤。” “怎么了栋猖?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵净薛,是天一觀的道長。 經(jīng)常有香客問我掂铐,道長罕拂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任全陨,我火速辦了婚禮爆班,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘辱姨。我一直安慰自己柿菩,他們只是感情好,可當我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布雨涛。 她就那樣靜靜地躺著枢舶,像睡著了一般。 火紅的嫁衣襯著肌膚如雪替久。 梳的紋絲不亂的頭發(fā)上凉泄,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天,我揣著相機與錄音蚯根,去河邊找鬼后众。 笑死,一個胖子當著我的面吹牛颅拦,可吹牛的內(nèi)容都是我干的蒂誉。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼距帅,長吁一口氣:“原來是場噩夢啊……” “哼右锨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起碌秸,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤绍移,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后讥电,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體登夫,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年允趟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸦致。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡潮剪,死狀恐怖涣楷,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情抗碰,我是刑警寧澤狮斗,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站弧蝇,受9級特大地震影響碳褒,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜看疗,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一沙峻、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧两芳,春花似錦摔寨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至竖螃,卻和暖如春淑廊,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背特咆。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工季惩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人坚弱。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓蜀备,卻偏偏與公主長得像,于是被迫代替她去往敵國和親荒叶。 傳聞我的和親對象是個殘疾皇子碾阁,可洞房花燭夜當晚...
    茶點故事閱讀 43,486評論 2 348

推薦閱讀更多精彩內(nèi)容