Python的生成器

昀溪發表於2018-08-26

如何產生一個列表呢?

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com


def main():
    list1 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    for i in list1:
        print(i)


if __name__ == '__main__':
    main()

透過上面的語句我們可以定義一個長度為10的列表,還有其他方法嗎?

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com


def main():
    list1 = [i for i in range(10)]
    for i in list1:
        print(i)


if __name__ == '__main__':
    main()

結果和上面是一樣的。那麼你想一個問題列表佔用記憶體空間,10個元素很少,但是假設有100萬個元素的列表,難道你要寫100萬個嗎?顯然你會用第二種方法,但是這裡有有一個問題,記憶體中放100萬個元素是不是太佔空間了?別以為你記憶體大,如果是10億呢?這裡就要用到列表生成器了。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com


def main():
    list1 = [i for i in range(10)]
    print(list1)

    # 透過列表生成器產生列表
    list2 = (i for i in range(10))
    print(list2)


if __name__ == '__main__':
    main()

看起來寫法差不多啊,其實區別就是把[]換成(),但是輸出你發現第一種輸出的是真實的列表,而第二種輸出的是一個生成器地址。這時候你想了,那我怎麼遍歷這個列表呢?其實和正常遍歷列表一樣生成器本身也是可以迭代的。下面我們說說列表生成器是什麼?

列表生成器是根據演算法推算列表元素,而不是直接定義,這樣你建立一個列表生成器而不是建立一個完整列表,當遍歷這個列表時,一邊遍歷一邊生成後面的元素,不能跳著訪問,真實的列表你可以透過指定下標來訪問任意位置,但是列表生成器只能透過遍歷挨個訪問,所以它也不能做切片。下面我們自定義一個生成器。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Author: rex.cheny
# E-mail: rex.cheny@outlook.com


# 自定義生成器
def consumtGenerator(max):
    """
    第一次呼叫這個函式將返回一個生成器而不是真正執行這裡面的方法,只有調這個生成器的next方法才會執行。
    yield 可以返回資料,我這裡返回 n 同時你也可以把yield賦值給一個變數,這樣可以接收資料  m = yield n。
    程式走到這裡遇到yield就返回了.返回後程式就停在這裡了。只有當喚醒之後才會執行下面的語句.
    """
    n = 0
    while n < max:
        yield n
        n += 1


def main():
    # 第一次呼叫函式,獲得一個生成器
    a = consumtGenerator(10)
    for i in a:
        print(i)


if __name__ == '__main__':
    main()

輸出結果是一樣的。這裡最重要的就是yield。執行過程如下:

  1. 初始化a成為一個生成器
  2. 第一次執行 for i in a 將第一次執行自定義生成器函式,n = 0 while這些語句,當遇到yeild時該函式返回n的值,此時是0,然後暫停
  3. 執行for語句中的列印語句
  4. 第二次執行for i in a 語句,將第二次執行自定義生成器函式,此時從上一次暫停的地方繼續執行也就是 n += 1,然後是while語句,之後再次遇到yeild語句,返回1,然後暫停
  5. 一次類推直到n = max

透過上例可以看出0-9這10個數字並沒有提前定義,而是給定一個演算法遍執行遍按照演算法計算然後輸出值。實際你可以感受到整個執行邏輯就是在自定義生成器和main函式直接來回切換,感覺很像單核心CPU上的多執行緒不也是來回切換利用速度和時間模擬併發麼。

透過生成器編寫消費者和生產者

#!/usr/bin/env python
# -*- coding: utf-8 -*-


def consumer(name, pices):
    print("--->[%s]等待骨頭,請餵我 %d 塊。" % (name, pices))
    eaten = 0
    while True:
        # 第一次呼叫這個函式將返回一個生成器而不是真正執行這裡面的方法,只有調這個生成器的next方法才會執行。
        # yield 可以返回資料,我這裡沒有返回。程式走到這裡遇到yield就返回了,這裡為什麼賦值給一個變數呢,因為可以接收資料。
        # 返回後程式就停在這裡了。只有當喚醒send之後才會執行下面的列印語句。
        bone = yield
        if eaten == pices:
            print("[%s] 我已經吃飽了。" % name)
        else:
            print("[%s] 吃了 %s 塊骨頭。" % (name, bone))
            eaten += 1
            print("[%s] 我已經吃了 %d 塊骨頭。" % (name, eaten))
        # time.sleep(1)


def producer():
    # 透過對生成器呼叫 next()的時候才會執行
    next(petDog1)
    next(petDog2)
    n = 0
    while n < 10:
        n += 1
        print("\033[32;1m[主人]\033[0m 丟 %s 塊骨頭。" % 1)
        # send是喚醒生成器,也就是讓函式繼續執行,這裡輸入一個引數,表示啟用這個生成器的時候給它傳遞一個變數進去,本例就是上面的 bone 這個變數,丟一塊骨頭
        petDog1.send(1)
        petDog2.send(1)


if __name__ == '__main__':
    # 函式里面有 yield 第一次呼叫這個函式它返回的是一個生成器,所以第一次不執行
    petDog1 = consumer("金毛", 10)
    petDog2 = consumer("泰迪", 3)
    master = producer()

泰迪吃了3塊就不吃了,金毛吃10塊。是不是有點多執行緒的意思。其實這是協程,至於協程是什麼會有單獨文章來講。

相關文章