Python的協程真的有那麼難嗎?

Python程式設計時光發表於2018-06-27

從今天開始,我們將開始進入Python的難點,那就是協程

為了寫明白協程的知識點,我查閱了網上的很多相關資料。發現很難有一個講得系統,講得全面的文章,導致我們在學習的時候,往往半知半解,學完還是一臉懵逼。

學習協程的第一門課程,是要認識生成器,有了生成器的基礎,才能更好地理解協程

如果你是新手,那麼你應該知道迭代器,對生成器應該是比較陌生的吧。沒關係,看完這系列文章,你也能從小白成功過渡為Ptyhon高手。

再次提醒
本系列所有的程式碼均在Python3下編寫,也建議大家儘快投入到Python3的懷抱中來。


. 可迭代、迭代器、生成器

初學Python的時候,對於這三貨真的是傻傻分不清。甚至還認為他們是等價的。

其實,他們是不一樣的。

可迭代的物件,很好理解,我們很熟悉的:字串listdicttupledeque

為了驗證我說的,需要藉助collections.abc這個模組(Python2沒有),使用isinstance()來類別一個物件是否是可迭代的(Iterable),是否是迭代器(Iterator),是否是生成器(Generator)。

import collections
from 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))

# deque
adeque=collections.deque('abcdefg')
print("deque:{}".format(adeque))
print(isinstance(adeque, Iterable))
print(isinstance(adeque, Iterator))
print(isinstance(adeque, Generator))
複製程式碼

輸出結果

字串:XiaoMing
True
False
False

列表:[21, 23, 32, 19]
True
False
False

字典:{'name': '小明', 'gender': '男', 'age': 18}
True
False
False

deque:deque(['a', 'b', 'c', 'd', 'e', 'f', 'g'])
True
False
False
複製程式碼

從結果來看,這些可迭代物件都不是迭代器,也不是生成器。它們有一個共同點,就是它們都可以使用for來迴圈。這一點,大家都知道,我們就不去驗證了。

擴充套件知識:
可迭代物件,是其內部實現了,__iter__ 這個魔術方法。
可以通過,dir()方法來檢視是否有__iter__來判斷一個變數是否是可迭代的。

接下來是,迭代器
對比可迭代物件,迭代器其實就只是多了一個函式而已。就是__next__(),我們可以不再使用for迴圈來間斷獲取元素值。而可以直接使用next()方法來實現。

迭代器,是在可迭代的基礎上實現的。要建立一個迭代器,我們首先,得有一個可迭代物件。
現在就來看看,如何建立一個可迭代物件,並以可迭代物件為基礎建立一個迭代器。

from collections.abc import Iterable, Iterator, Generator

class 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 StopIteration


if __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))
複製程式碼

輸出

0
1
2
3
4

True
False

True
True

0
1
2
3
4
複製程式碼

如果上面的程式碼太多,也可以看這邊,你更能理解。

from collections.abc import Iterator

aStr = 'abcd'  # 建立字串,它是可迭代物件
aIterator = iter(aStr)  # 通過iter(),將可迭代物件轉換為一個迭代器
print(isinstance(aIterator, Iterator))  # True
next(aIterator)  # a
next(aIterator)  # b
next(aIterator)  # c
next(aIterator)  # d
複製程式碼

擴充套件知識:
迭代器,是其內部實現了,__next__ 這個魔術方法。(Python3.x)
可以通過,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 += 1

if __name__ == '__main__':
    gen = mygen(10)
    print(isinstance(gen, Generator))  # True
複製程式碼

可迭代物件和迭代器,是將所有的值都生成存放在記憶體中,而生成器則是需要元素才臨時生成,節省時間,節省空間。


. 如何執行/啟用生成器

由於生成器並不是一次生成所有元素,而是一次一次的執行返回,那麼如何刺激生成器執行(或者說啟用)呢?

啟用主要有兩個方法

  • 使用next()
  • 使用generator.send(None)

分別看下例子,你就知道了。

def mygen(n):
    now = 0
    while now < n:
        yield now
        now += 1

if __name__ == '__main__':
    gen = mygen(4)

    # 通過交替執行,來說明這兩種方法是等價的。
    print(gen.send(None))
    print(next(gen))
    print(gen.send(None))
    print(next(gen))
複製程式碼

輸出

0
1
2
3
複製程式碼


. 生成器的執行狀態

生成器在其生命週期中,會有如下四個狀態

GEN_CREATED # 等待開始執行
GEN_RUNNING # 直譯器正在執行(只有在多執行緒應用中才能看到這個狀態)
GEN_SUSPENDED # 在yield表示式處暫停
GEN_CLOSED # 執行結束

通過程式碼來感受一下,為了不增加程式碼理解難度,GEN_RUNNING這個狀態,我就不舉例了。有興趣的同學,可以去嘗試一下多執行緒。若有疑問,可在後臺回覆我。

from inspect import getgeneratorstate

def mygen(n):
    now = 0
    while now < n:
        yield now
        now += 1

if __name__ == '__main__':
    gen = mygen(2)
    print(getgeneratorstate(gen))

    print(next(gen))
    print(getgeneratorstate(gen))

    print(next(gen))
    gen.close()  # 手動關閉/結束生成器
    print(getgeneratorstate(gen))
複製程式碼

輸出

GEN_CREATED
0
GEN_SUSPENDED
1
GEN_CLOSED
複製程式碼


. 生成器的異常處理

在生成器工作過程中,若生成器不滿足生成元素的條件,就/應該 丟擲異常(StopIteration)。

通過列表生成式構建的生成器,其內部已經自動幫我們實現了丟擲異常這一步。不信我們來看一下。

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

def mygen(n):
    now = 0
    while now < n:
        yield now
        now += 1
    raise StopIteration

if __name__ == '__main__':
    gen = mygen(2)
    next(gen)
    next(gen)
    next(gen)
複製程式碼


. 從生成器過渡到協程: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 += jump

if __name__ == '__main__':
    itr = jumping_range(5)
    print(next(itr))
    print(itr.send(2))
    print(next(itr))
    print(itr.send(-1))
複製程式碼

輸出。

0
2
3
2
複製程式碼

這裡解釋下為什麼這麼輸出。
重點是jump = yield index這個語句。

分成兩部分:

  • yield index 是將index return給外部呼叫程式。
  • jump = yield 可以接收外部程式通過send()傳送的資訊,並賦值給jump

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

下一章,我將講一個Python3.5新引入的語法:yield from。篇幅也比較多,所以就單獨拿出來講。

                                        關注公眾號,獲取最新文章
                                                        關注公眾號,獲取最新文章


相關文章