理解Python協(xié)程:從yield/send到y(tǒng)ield from再到async/await

Python中的協(xié)程大概經(jīng)歷了如下三個階段:

  1. 最初的生成器變形yield/send
  2. 引入@asyncio.coroutine和yield from
  3. 在最近的Python3.5版本中引入async/await關(guān)鍵字

一垦藏、生成器變形yield/send
普通函數(shù)中如果出現(xiàn)了yield關(guān)鍵字廷臼,那么該函數(shù)就不再是普通函數(shù)搔确,而是一個生成器陪竿。

def mygen(alist):
    while len(alist) > 0:
        c = randint(0, len(alist)-1)
        yield alist.pop(c)
a = ["aa","bb","cc"]
c=mygen(a)
print(c)

// 輸出:<generator object mygen at 0x02E5BF00>

像上面代碼中的c就是一個生成器。生成器就是一種迭代器简卧,可以使用for進(jìn)行迭代兔魂。生成器函數(shù)最大的特點是可以接受外部傳入的一個變量,并根據(jù)變量內(nèi)容計算結(jié)果后返回举娩。
這一切都是靠生成器內(nèi)部的send()函數(shù)實現(xiàn)的析校。

def gen():
    value=0
    while True:
        receive=yield value
        if receive=='e':
            break
        value = 'got: %s' % receive

g=gen()
print(g.send(None))    
print(g.send('hello'))
print(g.send(123456))
print(g.send('e'))

上面生成器函數(shù)中最關(guān)鍵也是最易理解錯的,就是receive=yield value這句,如果對循環(huán)體的執(zhí)行步驟理解錯誤铜涉,就會失之毫厘智玻,差之千里。
其實receive=yield value包含了3個步驟:
1芙代、向函數(shù)外拋出(返回)value
2尚困、暫停(pause),等待next()或send()恢復(fù)
3链蕊、賦值receive=MockGetValue() 事甜。 這個MockGetValue()是假想函數(shù),用來接收send()發(fā)送進(jìn)來的值

執(zhí)行流程:
1滔韵、通過g.send(None)或者next(g)啟動生成器函數(shù)逻谦,并執(zhí)行到第一個yield語句結(jié)束的位置。這里是關(guān)鍵陪蜻,很多人就是在這里搞糊涂的邦马。運行receive=yield value語句時,我們按照開始說的拆開來看宴卖,實際程序只執(zhí)行了1滋将,2兩步,程序返回了value值症昏,并暫停(pause)随闽,并沒有執(zhí)行第3步給receive賦值。因此yield value會輸出初始值0肝谭。這里要特別注意:在啟動生成器函數(shù)時只能send(None),如果試圖輸入其它的值都會得到錯誤提示信息掘宪。

2、通過g.send('hello')攘烛,會傳入hello魏滚,從上次暫停的位置繼續(xù)執(zhí)行,那么就是運行第3步坟漱,賦值給receive鼠次。然后計算出value的值,并回到while頭部,遇到y(tǒng)ield value腥寇,程序再次執(zhí)行了1成翩,2兩步,程序返回了value值花颗,并暫停(pause)捕传。此時yield value會輸出”got: hello”惠拭,并等待send()激活扩劝。

3、通過g.send(123456)职辅,會重復(fù)第2步棒呛,最后輸出結(jié)果為”got: 123456″。

4域携、當(dāng)我們g.send(‘e’)時簇秒,程序會執(zhí)行break然后推出循環(huán),最后整個函數(shù)執(zhí)行完畢秀鞭,所以會得到StopIteration異常趋观。

從上面可以看出, 在第一次send(None)啟動生成器(執(zhí)行1–>2锋边,通常第一次返回的值沒有什么用)之后皱坛,對于外部的每一次send(),生成器的實際在循環(huán)中的運行順序是3–>1–>2豆巨,也就是先獲取值剩辟,然后dosomething,然后返回一個值往扔,再暫停等待贩猎。

二、yield from
看一段代碼:

def g1():     
     yield  range(5)
def g2():
     yield  from range(5)

it1 = g1()
it2 = g2()
for x in it1:
    print(x)

for x in it2:
    print(x)


輸出: 
range(0, 5) 
0 
1 
2 
3 
4

這說明yield就是將range這個可迭代對象直接返回了萍膛。
而yield from解析了range對象吭服,將其中每一個item返回了。
yield from iterable本質(zhì)上等于for item in iterable: yield item的縮寫版
來看一下例子蝗罗,假設(shè)我們已經(jīng)編寫好一個斐波那契數(shù)列函數(shù)

def fab(max):
     n,a,b = 0,0,1
     while n < max:
          yield b
          # print b
          a, b = b, a + b
          n = n + 1
f=fab(5) 

fab不是一個普通函數(shù)噪馏,而是一個生成器。因此fab(5)并沒有執(zhí)行函數(shù)绿饵,而是返回一個生成器對象(生成器一定是迭代器iterator欠肾,迭代器一定是可迭代對象iterable)
現(xiàn)在我們來看一下,假設(shè)要在fab()的基礎(chǔ)上實現(xiàn)一個函數(shù)拟赊,調(diào)用起始都要記錄日志

def f_wrapper(fun_iterable):
    print('start')
    for item  in fun_iterable:
        yield item
     print('end')
wrap = f_wrapper(fab(5))
for i in wrap:
    print(i,end=' ')

現(xiàn)在使用yield from代替for循環(huán)

import logging
def f_wrapper2(fun_iterable):
    print('start')
    yield from fun_iterable  #注意此處必須是一個可生成對象
    print('end')
wrap = f_wrapper2(fab(5))
for i in wrap:
    print(i,end=' ')

再強(qiáng)調(diào)一遍:yield from后面必須跟iterable對象(可以是生成器刺桃,迭代器)

三、asyncio.coroutine和yield from
yield from在asyncio模塊中得以發(fā)揚光大吸祟。之前都是我們手工切換協(xié)程瑟慈,現(xiàn)在當(dāng)聲明函數(shù)為協(xié)程后桃移,我們通過事件循環(huán)來調(diào)度協(xié)程。

先看示例代碼:

import asyncio,random
@asyncio.coroutine
def smart_fib(n):
    index = 0
    a = 0
    b = 1
    while index < n:
        sleep_secs = random.uniform(0, 0.2)
        yield from asyncio.sleep(sleep_secs) #通常yield from后都是接的耗時操作
        print('Smart one think {} secs to get {}'.format(sleep_secs, b))
        a, b = b, a + b
        index += 1

@asyncio.coroutine
def stupid_fib(n):
    index = 0
    a = 0
    b = 1
    while index < n:
        sleep_secs = random.uniform(0, 0.4)
        yield from asyncio.sleep(sleep_secs) #通常yield from后都是接的耗時操作
        print('Stupid one think {} secs to get {}'.format(sleep_secs, b))
        a, b = b, a + b
        index += 1

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    tasks = [
        smart_fib(10),
        stupid_fib(10),
    ]
    loop.run_until_complete(asyncio.wait(tasks))
    print('All fib finished.')
    loop.close()

yield from語法可以讓我們方便地調(diào)用另一個generator葛碧。
本例中yield from后面接的asyncio.sleep()是一個coroutine(里面也用了yield from)借杰,所以線程不會等待asyncio.sleep(),而是直接中斷并執(zhí)行下一個消息循環(huán)进泼。當(dāng)asyncio.sleep()返回時蔗衡,線程就可以從yield from拿到返回值(此處是None),然后接著執(zhí)行下一行語句乳绕。
asyncio是一個基于事件循環(huán)的實現(xiàn)異步I/O的模塊绞惦。通過yield from,我們可以將協(xié)程asyncio.sleep的控制權(quán)交給事件循環(huán)洋措,然后掛起當(dāng)前協(xié)程济蝉;之后,由事件循環(huán)決定何時喚醒a(bǔ)syncio.sleep,接著向后執(zhí)行代碼菠发。
協(xié)程之間的調(diào)度都是由事件循環(huán)決定王滤。
yield from asyncio.sleep(sleep_secs) 這里不能用time.sleep(1)因為time.sleep()返回的是None,它不是iterable滓鸠,還記得前面說的yield from后面必須跟iterable對象(可以是生成器雁乡,迭代器)。
所以會報錯:

yield from time.sleep(sleep_secs) 
TypeError: ‘NoneType’ object is not iterable

四哥力、async和await
弄清楚了asyncio.coroutine和yield from之后蔗怠,在Python3.5中引入的async和await就不難理解了:可以將他們理解成asyncio.coroutine/yield from的完美替身。當(dāng)然吩跋,從Python設(shè)計的角度來說寞射,async/await讓協(xié)程表面上獨立于生成器而存在,將細(xì)節(jié)都隱藏于asyncio模塊之下锌钮,語法更清晰明了桥温。
加入新的關(guān)鍵字 async ,可以將任何一個普通函數(shù)變成協(xié)程

import time,asyncio,random
async def mygen(alist):
    while len(alist) > 0:
        c = randint(0, len(alist)-1)
        print(alist.pop(c))
a = ["aa","bb","cc"]
c=mygen(a)
print(c)
輸出:
<coroutine object mygen at 0x02C6BED0>

在上面程序中梁丘,我們在前面加上async侵浸,該函數(shù)就變成一個協(xié)程了。

但是async對生成器是無效的氛谜。async無法將一個生成器轉(zhuǎn)換成協(xié)程掏觉。
還是剛才那段代碼,我們把print改成yield

async def mygen(alist):
    while len(alist) > 0:
        c = randint(0, len(alist)-1)
        yield alist.pop(c)
a = ["ss","dd","gg"]
c=mygen(a)
print(c)

可以看到輸出

<async_generator object mygen at 0x02AA7170>
1
并不是coroutine 協(xié)程對象

所以我們的協(xié)程代碼應(yīng)該是這樣的

import time,asyncio,random
async def mygen(alist):
    while len(alist) > 0:
        c = random.randint(0, len(alist)-1)
        print(alist.pop(c))
        await asyncio.sleep(1) 
strlist = ["ss","dd","gg"]
intlist=[1,2,5,6]
c1=mygen(strlist)
c2=mygen(intlist)
print(c1)

要運行協(xié)程值漫,要用事件循環(huán)
在上面的代碼下面加上:

if __name__ == '__main__':
        loop = asyncio.get_event_loop()
        tasks = [
        c1,
        c2
        ]
        loop.run_until_complete(asyncio.wait(tasks))
        print('All fib finished.')
        loop.close()

就可以看到交替執(zhí)行的效果澳腹。

本文參考
http://python.jobbole.com/81911/
http://python.jobbole.com/86069/
原文:https://blog.csdn.net/soonfly/article/details/78361819

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子酱塔,更是在濱河造成了極大的恐慌沥邻,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,888評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件羊娃,死亡現(xiàn)場離奇詭異唐全,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蕊玷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,677評論 3 399
  • 文/潘曉璐 我一進(jìn)店門邮利,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人集畅,你說我怎么就攤上這事近弟∶逶悖” “怎么了瓜富?”我有些...
    開封第一講書人閱讀 168,386評論 0 360
  • 文/不壞的土叔 我叫張陵捅暴,是天一觀的道長。 經(jīng)常有香客問我,道長殴穴,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,726評論 1 297
  • 正文 為了忘掉前任辞色,我火速辦了婚禮鸽嫂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘髓窜。我一直安慰自己扇苞,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,729評論 6 397
  • 文/花漫 我一把揭開白布寄纵。 她就那樣靜靜地躺著鳖敷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪程拭。 梳的紋絲不亂的頭發(fā)上定踱,一...
    開封第一講書人閱讀 52,337評論 1 310
  • 那天,我揣著相機(jī)與錄音恃鞋,去河邊找鬼崖媚。 笑死,一個胖子當(dāng)著我的面吹牛恤浪,可吹牛的內(nèi)容都是我干的畅哑。 我是一名探鬼主播,決...
    沈念sama閱讀 40,902評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼水由,長吁一口氣:“原來是場噩夢啊……” “哼荠呐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,807評論 0 276
  • 序言:老撾萬榮一對情侶失蹤直秆,失蹤者是張志新(化名)和其女友劉穎濒募,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體圾结,經(jīng)...
    沈念sama閱讀 46,349評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡瑰剃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,439評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了筝野。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片晌姚。...
    茶點故事閱讀 40,567評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖歇竟,靈堂內(nèi)的尸體忽然破棺而出挥唠,到底是詐尸還是另有隱情,我是刑警寧澤焕议,帶...
    沈念sama閱讀 36,242評論 5 350
  • 正文 年R本政府宣布宝磨,位于F島的核電站,受9級特大地震影響盅安,放射性物質(zhì)發(fā)生泄漏唤锉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,933評論 3 334
  • 文/蒙蒙 一别瞭、第九天 我趴在偏房一處隱蔽的房頂上張望窿祥。 院中可真熱鬧,春花似錦蝙寨、人聲如沸晒衩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,420評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽听系。三九已至,卻和暖如春箱亿,著一層夾襖步出監(jiān)牢的瞬間跛锌,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,531評論 1 272
  • 我被黑心中介騙來泰國打工届惋, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留髓帽,地道東北人。 一個月前我還...
    沈念sama閱讀 48,995評論 3 377
  • 正文 我出身青樓脑豹,卻偏偏與公主長得像郑藏,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子瘩欺,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,585評論 2 359

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