天天看點

python yield了解_詳解Python 的生成器與協程

從今天開始,我們将開始進入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

來循環。這一點,大家都知道,我們就不去驗證了。

關于可疊代對象,有幾點需要補充說明

  1. 可以通過,

    dir()

    方法檢視,若有有

    __iter__

    說明是可疊代的,但是如果沒有,也不能說明不可疊代,原因見第二條。
  2. 判斷是否可疊代,不能僅看是否有

    __iter__

    來草率決定,因為隻實作了

    __getitem__

    方法的也有可能是可疊代的。因為當沒有

    __iter__

    時, Python 解釋器會去找

    __getitem__

    ,嘗試按順序(從索引0開始)擷取元素,不抛異常,即是可疊代。
  3. 是以,最好的判斷方法應該是通過

    for循環

    或者

    iter()

    去真實運作。
python yield了解_詳解Python 的生成器與協程

接下來是,

疊代器

。對比可疊代對象,

疊代器

其實就隻是多了一個函數而已。就是

__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
           

補充說明:

  1. 疊代器,是其内部實作了,

    __next__

    這個魔術方法。(Python3.x)
  2. 可以通過,

    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

# 解釋器正在執行(隻有在多線程應用中才能看到這個狀态)

GEN_SUSPENDED

# 在yield表達式處暫停

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

)。

通過清單生成式建構的生成器,其内部已經自動幫我們實作了抛出異常這一步。不信我們來看一下。

python yield了解_詳解Python 的生成器與協程

是以我們在自己定義一個生成器的時候,我們也應該在不滿足生成元素條件的時候,抛出異常。拿上面的代碼來修改一下。

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

這個語句。

分成兩部分:

  • yield index

    是将index

    return

    給外部調用程式。
  • jump = yield

    可以接收外部程式通過send()發送的資訊,并指派給

    jump

以上這些,都是講協程并發的基礎必備知識,請一定要親自去實踐并了解它,不然後面的内容,将會變得枯燥無味,晦澀難懂。

文末福利

本人原創的 《PyCharm 中文指南》一書前段時間一經釋出,就火爆了整個 Python 圈,釋出僅一天的時間,下載下傳量就突破了 1000 ,并且在當天就在 Github 上就收獲了數百的 star,截至目前,下載下傳量已經破萬。

這本書一共将近 200 頁,内含大量的圖解,制作之精良,值得每個 Python 工程師 人手一份。

python yield了解_詳解Python 的生成器與協程

為友善你下載下傳,我将這本書上傳到 百度網盤上了,點贊 + 轉發 本篇文章後可自行擷取。

連結:https://pan.baidu.com/s/1-NzATHFtaTV1MQzek70iUQ密碼:mft3

繼續閱讀