從今天開始,我們将開始進入Python的難點,那就是
協程
。
為了寫明白協程的知識點,我查閱了網上的很多相關資料。發現很難有一個講得系統,講得全面的文章,導緻我們在學習的時候,往往半知半解,學完還是一臉懵逼。
學習協程的第一門課程,是要認識
生成器
,有了
生成器
的基礎,才能更好地了解
協程
。
如果你是新手,那麼你應該知道
疊代器
,對
生成器
應該是比較陌生的吧。沒關系,看完這系列文章,你也能從小白成功過渡為Ptyhon高手。
1. 可疊代、疊代器、生成器
初學Python的時候,對于這三貨真的是傻傻分不清。甚至還認為他們是等價的。
其實,他們是不一樣的。
可疊代的對象,很好了解,我們很熟悉的:
字元串
,
list
,
dict
,
tuple
,
deque
等
為了驗證我說的,需要借助
collections.abc
這個子產品(Python2沒有),使用
isinstance()
來類别一個對象是否是可疊代的(
Iterable
),是否是疊代器(
Iterator
),是否是生成器(
Generator
)。
這幾個判斷方法,在這裡适用,但并不是絕對适用,原因見後面補充說明。
import collectionsfrom collections.abc import Iterable, Iterator, Generator# 字元串astr = "XiaoMing"print("字元串:{}".format(astr))print(isinstance(astr, Iterable))print(isinstance(astr, Iterator))print(isinstance(astr, Generator))# 清單alist = [21, 23, 32,19]print("清單:{}".format(alist))print(isinstance(alist, Iterable))print(isinstance(alist, Iterator))print(isinstance(alist, Generator))# 字典adict = {"name": "小明", "gender": "男", "age": 18}print("字典:{}".format(adict))print(isinstance(adict, Iterable))print(isinstance(adict, Iterator))print(isinstance(adict, Generator))# dequeadeque=collections.deque('abcdefg')print("deque:{}".format(adeque))print(isinstance(adeque, Iterable))print(isinstance(adeque, Iterator))print(isinstance(adeque, Generator))
輸出結果
字元串:XiaoMingTrueFalseFalse清單:[21, 23, 32, 19]TrueFalseFalse字典:{'name': '小明', 'gender': '男', 'age': 18}TrueFalseFalsedeque:deque(['a', 'b', 'c', 'd', 'e', 'f', 'g'])TrueFalseFalse
從結果來看,這些可疊代對象都不是疊代器,也不是生成器。它們有一個共同點,就是它們都可以使用
for
來循環。這一點,大家都知道,我們就不去驗證了。
關于可疊代對象,有幾點需要補充說明
- 可以通過,
方法檢視,若有有dir()
說明是可疊代的,但是如果沒有,也不能說明不可疊代,原因見第二條。__iter__
- 判斷是否可疊代,不能僅看是否有
來草率決定,因為隻實作了__iter__
方法的也有可能是可疊代的。因為當沒有__getitem__
時, Python 解釋器會去找__iter__
,嘗試按順序(從索引0開始)擷取元素,不抛異常,即是可疊代。__getitem__
- 是以,最好的判斷方法應該是通過
或者for循環
去真實運作。iter()
接下來是,
疊代器
。對比可疊代對象,
疊代器
其實就隻是多了一個函數而已。就是
__next__()
,我們可以不再使用
for
循環來間斷擷取元素值。而可以直接使用next()方法來實作。
疊代器,是在可疊代的基礎上實作的。要建立一個疊代器,我們首先,得有一個可疊代對象。現在就來看看,如何建立一個可疊代對象,并以可疊代對象為基礎建立一個疊代器。
from collections.abc import Iterable, Iterator, Generatorclass MyList(object): # 定義可疊代對象類 def __init__(self, num): self.end = num # 上邊界 # 傳回一個實作了__iter__和__next__的疊代器類的執行個體 def __iter__(self): return MyListIterator(self.end)class MyListIterator(object): # 定義疊代器類 def __init__(self, end): self.data = end # 上邊界 self.start = 0 # 傳回該對象的疊代器類的執行個體;因為自己就是疊代器,是以傳回self def __iter__(self): return self # 疊代器類必須實作的方法,若是Python2則是next()函數 def __next__(self): while self.start < self.data: self.start += 1 return self.start - 1 raise StopIterationif __name__ == '__main__': my_list = MyList(5) # 得到一個可疊代對象 print(isinstance(my_list, Iterable)) # True print(isinstance(my_list, Iterator)) # False # 疊代 for i in my_list: print(i) my_iterator = iter(my_list) # 得到一個疊代器 print(isinstance(my_iterator, Iterable)) # True print(isinstance(my_iterator, Iterator)) # True # 疊代 print(next(my_iterator)) print(next(my_iterator)) print(next(my_iterator)) print(next(my_iterator)) print(next(my_iterator))
輸出
01234TrueFalseTrueTrue01234
如果上面的代碼太多,也可以看這邊,你更能了解。
from collections.abc import IteratoraStr = 'abcd' # 建立字元串,它是可疊代對象aIterator = iter(aStr) # 通過iter(),将可疊代對象轉換為一個疊代器print(isinstance(aIterator, Iterator)) # Truenext(aIterator) # anext(aIterator) # bnext(aIterator) # cnext(aIterator) # d
補充說明:
- 疊代器,是其内部實作了,
這個魔術方法。(Python3.x)__next__
- 可以通過,
方法來檢視是否有dir()
來判斷一個變量是否是疊代器的。__next__
接下來,是我們的重點,
生成器
。
生成器的概念在 Python 2.2 中首次出現,之是以引入生成器,是為了實作一個在計算下一個值時不需要浪費空間的結構。
前面我們說,疊代器,是在可疊代的基礎上,加了一個next()方法。而生成器,則是在疊代器的基礎上(
可以用for循環,可以使用next()
),再實作了
yield
。
yield
是什麼東西呢,它相當于我們函數裡的return。在每次next(),或者for周遊的時候,都會yield這裡将新的值傳回回去,并在這裡阻塞,等待下一次的調用。正是由于這個機制,才使用生成器在Python程式設計中大放異彩。實作節省記憶體,實作異步程式設計。
如何建立一個生成器,主要有如下兩種方法
- 使用清單生成式
# 使用清單生成式,注意不是[],而是()L = (x * x for x in range(10))print(isinstance(L, Generator)) # True
- 實作yield的函數
# 實作了yield的函數def mygen(n): now = 0 while now < n: yield now now += 1if __name__ == '__main__': gen = mygen(10) print(isinstance(gen, Generator)) # True
可疊代對象和疊代器,是将所有的值都生成存放在記憶體中,而
生成器
則是需要元素才臨時生成,節省時間,節省空間。
2. 如何運作/激活生成器
由于生成器并不是一次生成所有元素,而是一次一次的執行傳回,那麼如何刺激生成器執行(或者說激活)呢?
激活主要有兩個方法
- 使用
next()
- 使用
generator.send(None)
分别看下例子,你就知道了。
def mygen(n): now = 0 while now < n: yield now now += 1if __name__ == '__main__': gen = mygen(4) # 通過交替執行,來說明這兩種方法是等價的。 print(gen.send(None)) print(next(gen)) print(gen.send(None)) print(next(gen))
輸出
0123
3. 生成器的執行狀态
生成器在其生命周期中,會有如下四個狀态
# 等待開始執行
GEN_CREATED
# 解釋器正在執行(隻有在多線程應用中才能看到這個狀态)
GEN_RUNNING
# 在yield表達式處暫停
GEN_SUSPENDED
# 執行結束
GEN_CLOSED
通過代碼來感受一下,為了不增加代碼了解難度,
GEN_RUNNING
這個狀态,我就不舉例了。有興趣的同學,可以去嘗試一下多線程。若有疑問,可在背景回複我。
from inspect import getgeneratorstatedef mygen(n): now = 0 while now < n: yield now now += 1if __name__ == '__main__': gen = mygen(2) print(getgeneratorstate(gen)) print(next(gen)) print(getgeneratorstate(gen)) print(next(gen)) gen.close() # 手動關閉/結束生成器 print(getgeneratorstate(gen))
輸出
GEN_CREATED0GEN_SUSPENDED1GEN_CLOSED
4. 生成器的異常處理
在生成器工作過程中,若生成器不滿足生成元素的條件,就
會
/
應該
抛出異常(
StopIteration
)。
通過清單生成式建構的生成器,其内部已經自動幫我們實作了抛出異常這一步。不信我們來看一下。
是以我們在自己定義一個生成器的時候,我們也應該在不滿足生成元素條件的時候,抛出異常。拿上面的代碼來修改一下。
def mygen(n): now = 0 while now < n: yield now now += 1 raise StopIterationif __name__ == '__main__': gen = mygen(2) next(gen) next(gen) next(gen)
5. 從生成器過渡到協程:yield
通過上面的介紹,我們知道生成器為我們引入了暫停函數執行(
yield
)的功能。當有了暫停的功能之後,人們就想能不能在生成器暫停的時候向其發送一點東西(其實上面也有提及:
send(None)
)。這種向暫停的生成器發送資訊的功能通過
PEP 342
進入
Python 2.5
中,并催生了
Python
中
協程
的誕生。根據
wikipedia
中的定義
協程是為非搶占式多任務産生子程式的計算機程式元件,協程允許不同入口點在不同位置暫停或開始執行程式。
注意從本質上而言,協程并不屬于語言中的概念,而是程式設計模型上的概念。
協程和線程,有
相似點
,多個協程之間和線程一樣,隻會交叉串行執行;也有
不同點
,線程之間要頻繁進行切換,加鎖,解鎖,從複雜度和效率來看,和協程相比,這确是一個痛點。協程通過使用
yield
暫停生成器,可以将程式的執行流程交給其他的子程式,進而實作不同子程式的之間的交替執行。
下面通過一個簡明的示範來看看,如何向生成器中發送消息。
def jumping_range(N): index = 0 while index < N: # 通過send()發送的資訊将指派給jump jump = yield index if jump is None: jump = 1 index += jumpif __name__ == '__main__': itr = jumping_range(5) print(next(itr)) print(itr.send(2)) print(next(itr)) print(itr.send(-1))
輸出。
0232
這裡解釋下為什麼這麼輸出。重點是
jump = yield index
這個語句。
分成兩部分:
-
是将indexyield index
給外部調用程式。return
-
可以接收外部程式通過send()發送的資訊,并指派給jump = yield
jump
以上這些,都是講協程并發的基礎必備知識,請一定要親自去實踐并了解它,不然後面的内容,将會變得枯燥無味,晦澀難懂。
文末福利
本人原創的 《PyCharm 中文指南》一書前段時間一經釋出,就火爆了整個 Python 圈,釋出僅一天的時間,下載下傳量就突破了 1000 ,并且在當天就在 Github 上就收獲了數百的 star,截至目前,下載下傳量已經破萬。
這本書一共将近 200 頁,内含大量的圖解,制作之精良,值得每個 Python 工程師 人手一份。
為友善你下載下傳,我将這本書上傳到 百度網盤上了,點贊 + 轉發 本篇文章後可自行擷取。
連結:https://pan.baidu.com/s/1-NzATHFtaTV1MQzek70iUQ密碼:mft3