Python進(jìn)階 - 高性能計(jì)算之協(xié)程

迭代器

可迭代對(duì)象

什么是可迭代對(duì)象

可迭代對(duì)象就是對(duì)象的類(lèi)中實(shí)現(xiàn)了__iter__方法的對(duì)象。對(duì)于可迭代對(duì)象,可以使用for循環(huán)直接從中取得元素。原生數(shù)據(jù)中的list, set, tuple, str都是可迭代對(duì)象。

如何判斷對(duì)象是否可以迭代

判斷對(duì)象是否可以迭代,可以用from collections import Iterable,然后用isinstance標(biāo)識(shí)符將對(duì)象與之對(duì)比,如果可以迭代,就會(huì)返回True。

如何讓自定義的類(lèi)成為可迭代對(duì)象

通過(guò)定義類(lèi)的__iter__方法,可以讓自定義的類(lèi)創(chuàng)建的對(duì)象可迭代,示例如下:

from collections import Iterable


class myInt(int):
    """"""

    def __init__(self, num):
        self._num = num

    def __iter__(self):
        for i in range(self._num):
            yield i


def main():
    some_int = myInt(10)
    print("myInt is iterable: ", isinstance(some_int, Iterable))
    # 輸出myInt is iterable:  True
    for i in some_int:
        print(i, end=' ')
    print()


if __name__ == '__main__':
    main()

迭代器的創(chuàng)建和調(diào)用

迭代器是訪問(wèn)元素集合的一種方式,迭代器是一個(gè)可以記住遍歷位置的對(duì)象。迭代器對(duì)象從集合的第一個(gè)元素開(kāi)始訪問(wèn),直到所有元素訪問(wèn)結(jié)束。迭代器只能前進(jìn),不能后退。類(lèi)似于C++中的forward_iterator。

一個(gè)迭代器有兩個(gè)基本要素:__iter__方法,__next__方法。__iter__方法返回迭代器自身,__next__方法返回集合中的下一個(gè)值,如果集合中沒(méi)有更多元素,則拋出StopIteration異常。

在實(shí)際使用迭代器時(shí),一般讓可迭代對(duì)象類(lèi)的__iter__方法返回其迭代器,而迭代器的__next__方法設(shè)定訪問(wèn)元素的順序,一個(gè)例子如下:

from collections import Iterable
from collections import Iterator


class MyList(object):  # 定義可迭代對(duì)象類(lèi)
    def __init__(self, num):
        self.data = num  # 迭代上邊界

    def __iter__(self):
        return MyListIterator(self.data)


class MyListIterator(object):  # 定義迭代器類(lèi)
    def __init__(self, data):
        self.data = data  # 迭代上邊界
        self.current = 0  # 當(dāng)前迭代值
        self.step = 2  # 迭代步長(zhǎng)

        
    def __iter__(self):
        return self

      
    def __next__(self):
        while self.current <= self.data - self.step:
            self.current += self.step
            return self.current
        raise StopIteration  # 迭代超出上邊界時(shí)拋出異常


def main():
    smObj = MyList(8)
    print("MyList() is iterable: ", isinstance(smObj, Iterable))
    smObj_iterator = iter(smObj)
    print("iter(Mylist()) is iterator: ", isinstance(smObj_iterator, Iterator))
    for i in smObj:
        print(i, end=' ')
    print()


if __name__ == '__main__':
    main()

for循環(huán)進(jìn)行調(diào)用時(shí),會(huì)調(diào)用smObj中的 __iter__方法,返回一個(gè)迭代器后,調(diào)用迭代器中的__next__方法獲取返回值。

這里要注意,如果不手動(dòng)設(shè)定越界后拋出StepIterationfor循環(huán)是不會(huì)終止的,而是不斷會(huì)取到None。

迭代器的作用

使用迭代器,可以只記錄在集合中生成下一個(gè)元素的方式,而不去記錄生成的具體結(jié)果。這樣在生成一個(gè)大集合時(shí),可以只占用很小的內(nèi)存空間,更加memory-efficient。

例如用迭代器生成斐波那契數(shù)列:

class Fibonacci(object):
    def __init__(self, n):
        self.prev_last_item = 0
        self.last_item = 1
        self.current = 0
        self.upper_bound = n

    def __iter__(self):
        return self

    def __next__(self):
        if self.current < 2:
            tmp = self.current
            self.current += 1
            return tmp
        elif self.current < self.upper_bound:
            self.current += 1
            tmp = self.prev_last_item + self.last_item
            self.prev_last_item, self.last_item = self.last_item, tmp
            return tmp
        else:
            raise StopIteration


def main():
    # 用生成器打印斐波那契數(shù)列的前n項(xiàng)
    n = 20
    fib = Fibonacci(n)
    for i in fib:
        print(i)


if __name__ == '__main__':
    main()

除了for以外的迭代器使用

除了使用for循環(huán)以外,其實(shí)在進(jìn)行類(lèi)型轉(zhuǎn)換時(shí),也會(huì)使用迭代器。如:

fib = Fibonacci(15)
print(list(fib)) # 轉(zhuǎn)換為列表,實(shí)際上會(huì)調(diào)用迭代器
print(tuple(fib)) # 轉(zhuǎn)換為元組,實(shí)際上會(huì)調(diào)用迭代器

所以實(shí)際上在這個(gè)過(guò)程中,是先生成一個(gè)空列表或者空元組,再調(diào)用迭代器的__next__方法,依次向內(nèi)填充數(shù)據(jù)。

生成器

什么是生成器

生成器就是一種特殊的迭代器。它是一個(gè)返回迭代器的函數(shù),只能用于迭代操作。

創(chuàng)建生成器的方式

  • 第一種方式:將列表推導(dǎo)式的[] 換成()
def main():
    G = (i * i for i in range(10))
    print(G) # 輸出<generator object main.<locals>.<genexpr> at 0x10ddb7228>


if __name__ == '__main__':
    main()

要從生成器中取值,可以使用next()函數(shù)。

  • 第二種方式:在函數(shù)中使用yield關(guān)鍵字

當(dāng)函數(shù)中包含yield關(guān)鍵字時(shí),那么創(chuàng)建的就是一個(gè)生成器,而非普通函數(shù)。函數(shù)會(huì)順序執(zhí)行,遇到return或者最后一行函數(shù)語(yǔ)句就返回。但是generator會(huì)在每次調(diào)用next()時(shí)執(zhí)行,遇到yield語(yǔ)句是暫停并返回一個(gè)值,再次執(zhí)行時(shí),<u>從上次返回的yield語(yǔ)句處執(zhí)行。</u>

例如:

from time import sleep


def Fibonacci(num):
    a, b = 0, 1
    cnt = 0
    while cnt < num:
        print("Before yield, current cnt: {}".format(cnt))
        yield a
        print("After yield")
        cnt += 1
        a, b = b, a + b


def main():
    G = Fibonacci(10)
    print(G)  # <generator object Fibonacci at 0x104079228>
    next(G)  # Before yield, current cnt: 0
    sleep(1)
    next(G)  # After yield
             # Before yield, current cnt: 1


if __name__ == '__main__':
    main()

可以看到在執(zhí)行next時(shí),是不會(huì)執(zhí)行到函數(shù)的最后一句的,而是到yield就暫停并返回了。

獲得生成器函數(shù)的返回值

在生成器函數(shù)中,如果要獲得其返回值,需要用異常捕獲到方式,如下所示:

from time import sleep


def Fibonacci(num):
    a, b = 0, 1
    cnt = 0
    while cnt < num:
        yield a
        cnt += 1
        a, b = b, a + b
    return "Done!"


def main():
    G = Fibonacci(10)
    try:
        while True:
            ret = next(G)
            print(ret)
    except Exception as ret:
        print(ret.value)


if __name__ == '__main__':
    main()

這樣就會(huì)在最后打印出函數(shù)的返回值Done

使用send操作生成器

除了使用next()函數(shù)啟動(dòng)生成器以外,還可以用send方法來(lái)使生成器運(yùn)行。它與next()的不同在于可以向生成器內(nèi)傳入一些值,形如generator().send(someValue)。這樣就使得我們可以在生成器運(yùn)行的過(guò)程中操作它,極大增加了生成器的靈活性。

例如:

def generator(num):
    """一個(gè)生成器,生成斐波那契數(shù)列"""
    a, b = 0, 1
    cnt = 0
    while cnt <= num:
        yield a
        a, b = b, a + b
        cnt += 1


def main():
    g = generator(20)
    try:
        while True:
            print(g.send(None)) # 此時(shí)并未傳值,效果等同于next(g)
    except StopIteration:
        pass


if __name__ == '__main__':
    main()

如果說(shuō)希望在打印的過(guò)程中,讓生成器重啟,可以修改上面的代碼:

def generator(num):
    """一個(gè)生成器,生成斐波那契數(shù)列"""
    a, b = 0, 1
    cnt = 0
    while cnt <= num:
        val = yield a # val用來(lái)接收send傳入的值
        # 根據(jù)val是否有傳入值,來(lái)決定是否重置
        if val is None:
            a, b = b, a + b
        else:
            a, b = 0, 1
            cnt = val
        cnt += 1


def main():
    g = generator(5)
    reset = 0
    try:
        while True:
            print(g.send(None)) # 此時(shí)并未傳值,效果等同于next(g)
            reset += 1
            # 在打印兩項(xiàng)之后,重新啟動(dòng)生成器
            if reset == 2:
                print(g.send(0)) # 讓函數(shù)內(nèi)的cnt重置為0
    except StopIteration:
        pass


if __name__ == '__main__':
    main()

此時(shí)的結(jié)果為:

0
1
0
1
1
2
3

可以看到按照我們的需要,重啟了生成器。

這里注意,yield返回的值對(duì)應(yīng)next函數(shù)的返回值,而send方法傳入的值對(duì)應(yīng)的是yield語(yǔ)句執(zhí)行完成,下次重新從這里開(kāi)始運(yùn)行后,yield表達(dá)式的值。也就是說(shuō),在運(yùn)行時(shí)用send傳入值時(shí),實(shí)際上第一次暫停,是在someVal = yield a的等式右邊,這里a被返回給了next函數(shù)作為返回值,而yield a表達(dá)式的值會(huì)等于send(someVal)中傳入的值。

另外需要注意的是,如果需要用到send方法傳入值,一定不能在生成器第一次運(yùn)行時(shí)(沒(méi)有調(diào)用過(guò)next或者是send(None))進(jìn)行傳值,因?yàn)楹瘮?shù)還沒(méi)有運(yùn)行到yield語(yǔ)句,沒(méi)有東西可以接收傳入值。

協(xié)程 - yield實(shí)現(xiàn)多任務(wù)

我們需要注意到yield關(guān)鍵字的重要特點(diǎn)之一是可以暫停函數(shù),利用這個(gè)特點(diǎn),可以用yield實(shí)現(xiàn)多任務(wù)并發(fā)。這里的原理非常類(lèi)似操作系統(tǒng)的時(shí)間片輪轉(zhuǎn)。

如以下例子:

def task1(num):
    while True:
        print("Now in task1 - %d" % num)
        num = yield


def task2(num):
    while True:
        print("Now in test2 - %d" % num)
        num = yield


def main():
    current_num = 0
    num = 10
    g1 = task1(current_num)
    g2 = task2(current_num)
    next(g1)
    next(g2)
    while current_num < num:
        current_num += 1
        g1.send(current_num)
        g2.send(current_num)


if __name__ == '__main__':
    main()

輸出結(jié)果如下:

Now in task1 - 0
Now in test2 - 0
Now in task1 - 1
Now in test2 - 1
Now in task1 - 2
Now in test2 - 2
Now in task1 - 3
Now in test2 - 3
Now in task1 - 4
Now in test2 - 4
Now in task1 - 5
Now in test2 - 5
Now in task1 - 6
Now in test2 - 6
Now in task1 - 7
Now in test2 - 7
Now in task1 - 8
Now in test2 - 8
Now in task1 - 9
Now in test2 - 9
Now in task1 - 10
Now in test2 - 10

協(xié)程的特點(diǎn)在于調(diào)用的資源非常少,僅相當(dāng)于調(diào)用了一個(gè)函數(shù)。但是弱點(diǎn)在于無(wú)法合理利用計(jì)算機(jī)的多CPU,實(shí)際上就是用一個(gè)線程模擬了多任務(wù)的同時(shí)進(jìn)行。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容