一、yield
學習協(xié)程的第一門課程,是要認識生成器,有了生成器的基礎,才能更好地理解協(xié)程。
如果你是新手,那么你應該知道迭代器,對生成器應該是比較陌生的吧。沒關系,看完這系列文章,你也能從小白成功過渡為Ptyhon高手。
本文主要從以下幾個方面來學習yield的知識點:
1:可迭代、迭代器、生成器
2:如何運行/激活生成器
3:生成器的執(zhí)行狀態(tài)
4:從生成器過渡到協(xié)程:yield
1:可迭代、迭代器、生成器
我們?nèi)绾螀^(qū)分區(qū)分一個對象是否是可迭代、迭代器、還是生成器呢?有一個簡單的辦法:
from collections.abc import Iterable, Iterator, Generator
isinstance(obj, Iterable)? ? ? ? # 可迭代對象
isinstance(obj,?Iterator)? ? ? ? #?迭代器
isinstance(obj,?Generator)? ? #?生成器
Iterable:一般在python中想字符串,list, dict, tuple, set, deque等都是可迭代對象,從表象上看他們都可以使用 for 來循壞迭代,但實際上他們并不是迭代器,也不是生成器。因為一個對象只要實現(xiàn)了__iter__ 方法的,均可稱為可迭代對象。
擴展知識:
可迭代對象,是其內(nèi)部實現(xiàn)了,__iter__?這個魔術方法。
可以通過,dir()方法來查看是否有__iter__來判斷一個變量是否是可迭代的。

Iterator:迭代器,一般對象只要實現(xiàn)了__next__ 與?__iter__ 方法的均可稱為生成器對象,因為它可以不用for循序來間斷的獲取元素值(next(obj)).
迭代器,是在可迭代的基礎上實現(xiàn)的。要創(chuàng)建一個迭代器,我們首先,得有一個可迭代對象。
注意:迭代器在元素值迭代結束的時候會拋出?StopIteration 異常,這是必要的。s = "1234abc"?
iterator = iter(s)
isinstance(iterator , Iterator)? # True
擴展知識:
迭代器,是其內(nèi)部實現(xiàn)了,__next__、__iter__ 這個魔術方法。(Python3.x)
可以通過,dir()方法來查看是否有__next__來判斷一個變量是否是迭代器的。
Generator:生成器,是在迭代器的基礎上(可以用for循環(huán),可以使用next()),再實現(xiàn)了yield。
yield?是什么東西呢,它相當于我們函數(shù)里的return。在每次next(),或者for遍歷的時候,都會yield這里將新的值返回回去,并在這里阻塞,等待下一次的調(diào)用。正是由于這個機制,才使用生成器在Python編程中大放異彩。實現(xiàn)節(jié)省內(nèi)存,實現(xiàn)異步編程
實現(xiàn)生成器的方法:
(1):?使用列表生成式
# 使用列表生成式,注意不是[],而是()
L = (x * x for x in range(10))
print(isinstance(L, Generator))? # True
(2): 實現(xiàn)了yield的函數(shù)
from inspect import getgeneratorstate
def mygen(n):?
? ? ? ?now = 0
? ? ? ? while now < n:
? ? ? ? ? ? ? ? r = yield now
? ? ? ? ? ? ? ? now += 1
? ? ? ?? raise StopIteration
StopIteration:在生成器工作過程中,若生成器不滿足生成元素的條件,就會拋出異常StopIteration,也應該拋出該異常。
注意:
(1):?一般使用for來循環(huán)迭代生成器,在生成器結束是python解釋器會在for結束后自動捕獲StopIteration異常,讓我們的程序沒有感知(2): 使用next(gen), 當next最后一個一個yield后,無論后面yield后面有沒有return都會拋出StopIteration;? 那么此時如何獲取生成器函數(shù)的返回值呢?你只需要在最后一次的next(gen),使用try...except?StopIteration as e即可, 返回值在e.value中。
? ??try:
? ? ? ??ret = next(gtw)
? ? except StopIteration as e:?
? ? ? ? print("GGG:", e.value)? ? ? ? # 函數(shù)沒有返回值,默認None
send(param): 當生成器使用send(param)是,注意以下部分:
a: gen.send(None),相當于next(next), 因為next就是不帶參數(shù),默認是send(None)
b: 在gen.close或者拋出StopIteration 之前使用gen.send(100) 或?gen.send("abc")
? ? r = yield now
此時r的值就是send發(fā)送的值。執(zhí)行流程如下:
(1):? gen =?mygen
(2): print(next(gen)?)? ?????????????# 此時執(zhí)行到r = yield now,在yield now時,print打印的值為0,生成器暫停并阻塞在yield處, now + 1 該處代碼不會執(zhí)行,因為暫停并阻塞了
(3): print(gen.send(100))? ? ? ?# 此時r = yield now,會先接收到send的參數(shù)值,r就是參數(shù)的值,程序將會恢復執(zhí)行yiled后面的代碼,直到再次遇到下一個yield?,? 此時print打印的值為1,程序再次會暫停并阻塞。注意:send在上一次yield暫停阻塞處,yield會先接收send的參數(shù)值,然后恢復執(zhí)行后面的程序,直到下一個yield

可迭代象和迭代器,是將所有的值都生成存放在內(nèi)存中,而生成器則是需要元素才臨時生成,節(jié)省時間,節(jié)省空間。
2:如何運行/激活生成器
由于生成器并不是一次生成所有元素,而是一次一次的執(zhí)行返回,那么如何刺激生成器執(zhí)行(或者說激活)呢?激活主要有兩個方法:
a: 使用next()? ? ? ? # 相當于gen.send(None) , 第一次啟動、激活只能是send(None) , send不能是其他函數(shù)
b: 使用generator.send(None)
3:?生成器的執(zhí)行狀態(tài)
from inspect import getgeneratorstate, isgeneratorfunction
使用inspect.getgeneratorstate就能判斷生成器的狀態(tài),一般在其生命周期中,會有如下四個狀態(tài):
GEN_CREATED?# 等待開始執(zhí)行
GEN_RUNNING?# 解釋器正在執(zhí)行(只有在多線程應用中才能看到這個狀態(tài))GEN_SUSPENDED?# 在yield表達式處暫停
GEN_CLOSED?# 執(zhí)行結束>>>??gen = mygen(2)
>>>?print("1:", getgeneratorstate(gen))? ? ? ? #?GEN_CREATED
>>>?print(next(gen))? ?# print(gen.send(None))
>>>?print("2:", getgeneratorstate(gen))? ? ? ? #?GEN_SUSPENDED
>>>?gen.close()
>>>?print("3:", getgeneratorstate(gen))? ? ? ? #?GEN_CLOSED
4:?從生成器過渡到協(xié)程:yield
通過上面的介紹,我們知道生成器為我們引入了暫停函數(shù)執(zhí)行(yield)的功能。當有了暫停的功能之后,人們就想能不能在生成器暫停的時候向其發(fā)送一點東西(其實上面也有提及:send(None))。這種向暫停的生成器發(fā)送信息的功能通過?PEP 342?進入?Python 2.5?中,并催生了?Python?中協(xié)程的誕生。
注意從本質(zhì)上而言,協(xié)程并不屬于語言中的概念,而是編程模型上的概念。
協(xié)程和線程,有相似點,多個協(xié)程之間和線程一樣,只會交叉串行執(zhí)行;也有不同點,線程之間要頻繁進行切換,加鎖,解鎖,從復雜度和效率來看,和協(xié)程相比,這確是一個痛點。協(xié)程通過使用?yield?暫停生成器,可以將程序的執(zhí)行流程交給其他的子程序,從而實現(xiàn)不同子程序的之間的交替執(zhí)行。
def jumping_range(N):
????????index = 0 while index < N:
? ? ? ? ????????# 通過send()發(fā)送的信息將賦值給
????????????????jump jump = yield index
? ? ? ? ? ? ? ? if jump is None:
? ? ? ? ? ? ? ? ? ? jump = 1
? ? ? ? ? ? ????index += jump
if __name__ == '__main__':
itr = jumping_range(5)
print(next(itr))? ? ? ? ? ? # 0
print(itr.send(2))? ? ? ? # 2
print(next(itr))? ? ? ? ? ? # 3
print(itr.send(-1))? ? ? ?# 2
這里解釋下為什么這么輸出。
重點是jump = yield index這個語句。
分成兩部分:
yield index?是將index?return給外部調(diào)用程序。
jump = yield?可以接收外部程序通過send()發(fā)送的信息,并賦值給jump
以上這些,都是講協(xié)程并發(fā)的基礎必備知識,請一定要親自去實踐并理解它,不然后面的內(nèi)容,將會變得枯燥無味,晦澀難懂。
二、yield from
yield from 所在的函數(shù)被稱為委托生成器,它主要為調(diào)用方和子生成器提供一個雙向通道;那么下面我們你主要從以下方面來講解yield from的相關知識:
1: 為什么要使用協(xié)程
2: yield from的用法詳解
3: 為什么要使用yield from
1: 為什么要使用協(xié)程
在使用yield from之前,請讀者把上面的yield的知識好好復習鞏固一下。
總的來說asyncio比線程優(yōu)越的地方就是:協(xié)程不像線程那樣需要頻繁進行上下文切換、加鎖、解鎖,這些過程,所以協(xié)程之間切換的時間開銷將大幅減小,效率上將大幅提高。對于爬蟲、讀寫文件、讀磁盤等這種非常耗時的IO來說更是如此
def? spider_xx(url):
????????html = get_html(url)
? ? ? ? ......
????????data = parse_html(html)
我們都知道,get_html()等待返回網(wǎng)頁是非常耗IO的,一個網(wǎng)頁還好,如果我們爬取的網(wǎng)頁數(shù)據(jù)極其龐大,這個等待時間就非常驚人,是極大的浪費。
聰明的程序員,當然會想如果能在get_html()這里暫停一下,不用傻乎乎地去等待網(wǎng)頁返回,而是去做別的事。等過段時間再回過頭來到剛剛暫停的地方,接收返回的html內(nèi)容,然后還可以接下去解析parse_html(html)。
利用常規(guī)的方法,幾乎是沒辦法實現(xiàn)如上我們想要的效果的。所以Python想得很周到,從語言本身給我們實現(xiàn)了這樣的功能,這就是yield語法??梢詫崿F(xiàn)在某一函數(shù)中暫停的效果。
試著思考一下,假如沒有協(xié)程,我們要寫一個并發(fā)程序。可能有以下問題
1)使用最常規(guī)的同步編程要實現(xiàn)異步并發(fā)效果并不理想,或者難度極高。
2)由于GIL鎖的存在,多線程的運行需要頻繁的加鎖解鎖,切換線程,這極大地降低了并發(fā)性能;
而協(xié)程的出現(xiàn),剛好可以解決以上的問題。它的特點有
協(xié)程是在單線程里實現(xiàn)任務的切換的
利用同步的方式去實現(xiàn)異步
不再需要鎖,提高了并發(fā)性能
2:yield from的用法
yield from?后面需要加的是可迭代對象,它可以是普通的可迭代對象,也可以是迭代器,甚至是生成器。
astr='ABC'? ? ? ? ? ? ? ? #?字符串
alist=[1,2,3]? ? ? ? ? ? ?#?列表
adict={"name":"wangbm","age":18}? ? ? ? #?字典
agen=(i for i in range(4,8))? ? ? ? ? ? ? ? ? ? ? ? #?生成器
def gen(*args, **kw):
????????for item in args:
? ? ? ? ????????for i in item:
? ? ? ? ? ? ? ? ? ? ? ? yield idef gen_from(*args, **kw):????????
????????for item in args:
? ??????????????yield from itemnew_list=gen(astr, alist, adict, agen)
print(list(new_list))????????????????????????????????# ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]new_gen_list=gen_from(astr, alist, adict, agen)
print(list(new_gen_list))? ? ? ? ? ? ? ? ? ? ? # ['A', 'B', 'C', 1, 2, 3, 'name', 'age', 4, 5, 6, 7]
當然上面只是小case, yield from的應用遠不僅僅如此。當?yield from?后面加上一個生成器后,就實現(xiàn)了生成的嵌套。
當然實現(xiàn)生成器的嵌套,并不是一定必須要使用yield from,而是使用yield from可以讓我們避免讓我們自己處理各種料想不到的異常,而讓我們專注于業(yè)務代碼的實現(xiàn),講解它之前,首先要知道這個幾個概念:
1、調(diào)用方:????????????調(diào)用委托生成器的客戶端(調(diào)用方)代碼
2、委托生成器:????包含yield from表達式的生成器函數(shù)
3、子生成器:? ? ? ? ?yield from 后面加的生成器函數(shù)
委托生成器的作用是:在調(diào)用方與子生成器之間建立一個雙向通道。
所謂的雙向通道是什么意思呢?調(diào)用方可以通過send()直接發(fā)送消息給子生成器,而子生成器yield的值,也是直接返回給調(diào)用方。
你可能會經(jīng)??吹接行┐a,還可以在yield from前面看到可以賦值。這是什么用法?
你可能會以為,子生成器yield回來的值,被委托生成器給攔截了。你可以親自寫個demo運行試驗一下,并不是你想的那樣。因為我們之前說了,委托生成器,只起一個橋梁作用,它建立的是一個雙向通道,它并沒有權利也沒有辦法,對子生成器yield回來的內(nèi)容做攔截。
from collections import namedtuple
Result = namedtuple('Result', 'count average')def get_average():
????????""" 子生成器 """
?????????total = 0.0
? ? ? ? count = 0
? ? ????average = None
????????while True:
????????????????# send 發(fā)送值給yield接收, yield 后面可以沒有參數(shù);
????????????????# 有參數(shù)時 yield average 是為了讓調(diào)用方迭代獲取a值,和 term 沒有關系
????????????????term = yield average
????????????????if term is None:
? ? ????????????????????break total += term
????????????????????????count += 1
????????????????????????average = total / count
? ? ? ? return Result(count, average)def delegate_gen(results, key):
????????""" 委托生成器 """
????????while True:
????????????????# 只有當生成器 get_average()結束,才會返回結果給results賦值
? ??????????????# 無 while True 拋 StopIteration print("grouper end")
????????????????results[key] = yield from get_average()?
????????????????# return results ???? # 有無 while True 都會拋 StopIterationdef call_main(data):
????????""" 調(diào)用方 """
????????results = {}
????????for key, values in data.items():
????????????????delegation = delegate_gen(results, key)
????????????????next(delegation) # 啟動/激活子生成器,第一次運行到 yield 阻塞暫停
????????????????for value in values:
????????????????????????delegation.send(value)
????????????????delegation.send(None) # 結束子生成器(return 了)
????????print(results)
代碼里面有幾個很重要的點,作如下講解:

1:啟動/激活子生成器,next(delegation) 與?delegation.send(None), send參數(shù)只能是None
2:yield from 對【調(diào)用方】與【子生成器】起到雙向通道的作用
3:子生成器結束時,子生成器的返回值為默認值或是其他,都會拋出?StopIteration 異常,但是yield from會自動處理子生成器的該異常,那么ret =?yield from delegate_gen(...) 中, ret就是子生成器gen()的返回值, 等價于:
????????????????try:
? ? ? ? ????????????????delegation.send(None)
????????????????except StopIteration as e:
???????????????????????ret = e.value
4: 關于委托生成器拋出?StopIteration 異常的說明:
? ? ? ? (1):yield from 【在】while True 里,當子生成器結束后,并接收到子生成器的返回值后,委托生成器【不會】再次拋出?StopIteration, 代碼如下:
? ???????????????while True:
? ???????????????????????yield from get_average()?
? ? ? ? (2): 如果yield from 【不在】while True 里,當子生成器結束后,并接收到子生成器的返回值后,?委托生成器【會】再次拋出??StopIteration, 代碼如下:
? ??????????????yield from get_average()?
? ? ? ? (3): 只要yield from 【不在】while True 里,當子生成器結束后,并接收到子生成器的返回值后, 無論委托生成器函數(shù)有無return(無return, 默認None)都【會】拋出??StopIteration
關于 yield from 的功能給出了一段偽代碼,如下所示:
#一些說明
"""
_i:子生成器,同時也是一個迭代器
_y:子生成器生產(chǎn)的值
_r:yield from 表達式最終的值
_s:調(diào)用方通過send()發(fā)送的值
_e:異常對象"""
?_i = iter(EXPR)
?try:
?????????_y = next(_i)
except StopIteration as _e:
?????????_r = _e.value
?else:
? ? ? ? while 1:
????????????????try:
????????????????????????_s = yield _y
????????????????except GeneratorExit as _e:
????????????????????????try:
????????????????????????????????_m = _i.close
????????????????????????except AttributeError:
????????????????????????????????pass
????????????????????????else:
????????????????????????????????_m()
????????????????????????raise _e
? ? ? ? ? ? ? ? except BaseException as _e:
????????????????????????_x = sys.exc_info()
????????????????????????try:
????????????????????????????????_m = _i.throw
????????????????????????except AttributeError:
????????????????????????????????raise _e
????????????????????????else:
????????????????????????????????try:
????????????????????????????????????????_y = _m(*_x)
????????????????????????????????except StopIteration as _e:
????????????????????????????????????????_r = _e.value
????????????????????????????????????????break
????????????????else:
????????????????????????try:
????????????????????????????????if _s is None:
????????????????????????????????????????_y = next(_i)
????????????????????????????????else: _y = _i.send(_s)
????????????????????????except StopIteration as _e:
????????????????????????????????_r = _e.value break
RESULT = _r
以上的代碼,稍微有點復雜,有興趣的同學可以結合以下說明去研究看看。
1: 迭代器(即可指子生成器)產(chǎn)生的值直接返還給調(diào)用者
2: 任何使用send()方法發(fā)給委派生產(chǎn)器(即外部生產(chǎn)器)的值被直接傳遞給迭代器。如果send值是None,則調(diào)用迭代器next()方法;如果不為None,則調(diào)用迭代器的send()方法。如果對迭代器的調(diào)用產(chǎn)生StopIteration異常,委派生產(chǎn)器恢復繼續(xù)執(zhí)行yield from后面的語句;若迭代器產(chǎn)生其他任何異常,則都傳遞給委派生產(chǎn)器。
3: 子生成器可能只是一個迭代器,并不是一個作為協(xié)程的生成器,所以它不支持.throw()和.close()方法,即可能會產(chǎn)生AttributeError 異常。
4: 除了GeneratorExit 異常外的其他拋給委派生產(chǎn)器的異常,將會被傳遞到迭代器的throw()方法。如果迭代器throw()調(diào)用產(chǎn)生了StopIteration異常,委派生產(chǎn)器恢復并繼續(xù)執(zhí)行,其他異常則傳遞給委派生產(chǎn)器。
5: 如果GeneratorExit異常被拋給委派生產(chǎn)器,或者委派生產(chǎn)器的close()方法被調(diào)用,如果迭代器有close()的話也將被調(diào)用。如果close()調(diào)用產(chǎn)生異常,異常將傳遞給委派生產(chǎn)器。否則,委派生產(chǎn)器將拋出GeneratorExit 異常。
6: 當?shù)鹘Y束并拋出異常時,yield from表達式的值是其StopIteration 異常中的第一個參數(shù)。
7: 一個生成器中的return expr語句將會從生成器退出并拋出 StopIteration(expr)異常。