《Python有什麼好學的》之生成器/迭代器

「Python有什麼好學的」這句話可不是反問句,而是問句哦。shell

主要是煎魚以爲太多的人以爲Python的語法較爲簡單,寫出來的代碼只要符合邏輯,不須要太多的學習便可,便可從一門其餘語言跳來用Python寫(固然這樣是好事,誰都但願入門簡單)。緩存

因而我便記錄一下,若是要學Python的話,到底有什麼好學的。記錄一下Python有什麼值得學的,對比其餘語言有什麼特別的地方,有什麼樣的代碼寫出來更Pythonic。一路回味,一路學習。app

爲何是斐波那契

談到生成器/迭代器,人們老是喜歡用斐波那契數列來舉例。函數

斐波那契數列,數學表示爲a(1)=0, a(2)=1, a(i)=a(i-1)+a(i-2) (i>=3):
0 1 1 2 3 5 8 13 21 ...
用一句話說,就是第三位數起,當前這位數是前兩位的和。

固然,使用斐波那契來舉例是一個很合適的選擇。學習

那麼,爲何說到生成器/迭代器就喜歡斐波那契,而不是其餘呢?大數據

斐波那契數列有一個特徵:當前這位數的值,能夠經過前兩位數的值推導出來。好比,我知道了第n位數是5,第n+1位數是8,那我就能夠輕易地得出,第n+2位必然是5+8=13。spa

即,斐波那契數是能夠經過推導式得出的。code

就是這樣一種相似於冥冥註定的感受:當前的平行空間,是由你以前的選擇決定的。並且是欽定好的,你接下來的每一步,其實都已經被決定了,由什麼決定呢,由你之前走過的路決定的。對象

那麼,換句話來講,即能由推導式得出的數列,其實均可以用來作生成器/迭代器的例子。例如,煎魚用一條式子y(n)=y(n-1)^2 + 1 (n>=1), y(1)=1,同樣能拿來當例子。內存

既然這樣,那就用y(n)=y(n-1)^2 + 1 (n>=1), y(1)=1吧。

原始低效的循環

一開始,人們的思想很簡單,若是要求y(n)=y(n-1)^2 + 1 (n>=1), y(1)=1中y數列的第13位,即y(13),那很簡單,就輪詢到13個就行了。

def y():
    y_current = 1
    n = 1
    while n < 13:
        y_current = y_current ** 2 + 1
        n += 1
        print(n, y_current)

輸出也挺長的,就截個圖算了:

這個時候,這個代碼是徹底夠用的。接下來,煎魚加點需求(PM般的獰笑):

  1. 暴露n的值,n值能夠做爲參數輸入,即我須要控制數列計算到哪
  2. 函數返回(輸出)整個數列

接下來,函數改爲:

def y(n_max):
    y_current = 0
    n = 0
    ret_list = []
    while n < n_max:
        y_current = y_current ** 2 + 1
        n += 1
        ret_list.append(y_current)

    return ret_list


if __name__ == '__main__':
    for i in y(13):
        print(i)

看起來沒什麼毛病,徹底符合要求。可是,問題出如今當函數的參數n_max較大的時候。要多大呢,煎魚嘗試輸出n_max=13000000,即:

if __name__ == '__main__':
    for i in y(13000000):
        print(i)

咱們看得出來,這個函數的計算量十分大,以煎魚當前的電腦配置(Macbook pro 2017 i5 8G RAM),等了一兩分鐘還沒結束,只好強行中斷了。

程序爲何卡那麼久呢。由於在函數的邏輯中,程序試圖將13000000個值都計算出來再返回list以供外接輪詢,並且這13000000個一個比一個難算(愈來愈大,指數增加)。同時,該list也佔了龐大的內存空間。

到了這個時候,煎魚終於要引入生成器了。

生成器的小試牛刀

其實煎魚就加入了一個yield,並稍做修改:

def y_with_yield(n_max):
    y_current = 0
    n = 0
    while n < n_max:
        y_current = y_current ** 2 + 1
        n += 1
        yield y_current
        
if __name__ == '__main__':
    for i in y_with_yield(13000000):
        print(i)

雖然屏幕滾動得很慢,可是起碼是在實時地滾動的。

加入了yield變成這樣,其實就是搞成了一個簡單的生成器。在這裏,生成器的做用有:

  1. for i in y_with_yield(13000000)的循環中,每一次循環程序纔會進入函數去計算,而不會把所有結果都計算出來再返回
  2. 因爲不會把所有結果都計算出來再返回,程序運行所需的內存也大幅地減小

暫時給出初步結論:

  1. 這個小生成器在較大數據的計算量時,有較大的優點
  2. 程序把推導式的計算經過yield分散了,下降了cpu和內存的壓力
  3. 若是沒有煎魚的需求,這一切都白搭且多餘

咱們再來看下生成器的其餘用途吧。

經過緩存機制讀取文件

在讀文件或處理文件時使用緩存是頗有必要的,由於咱們老是不知道文件會有多大,文件的大小會不會把程序給拖垮。

煎魚新建一個文件(假設叫test.txt),並往其中寫入文本,運行如下代碼:

def read_file(file_path):

    BLOCK_SIZE = 100
    with open(file_path, 'rb') as f:
        while True:
            block = f.read(BLOCK_SIZE)
            if block:
                yield block
            else:
                return


if __name__ == '__main__':
    for i in read_file('./test.txt'):
        print(i)
        print('--------------block-split--------------')

咱們把100個長度分爲一個block,這個block就至關於咱們的緩存:先從文件中讀100個,而後讓程序處理這100個字符(此到處理爲print),再讀下一個100。其中block-split的輸出是爲了讓咱們更好地辯識出block的頭尾。

生成器類和生成器對象

經過yield瞎搞出來的簡易生成器有一個很大的限制,就是必需要在循環內。

雖然「迭代」和「循環」有關聯,可是當生成器的生成邏輯無比複雜時,好比「推導」的方法已經沒法用數學推導式表達時,或者某種場景下的業務邏輯比較複雜以致於沒法直接經過循環表達時,生成器類來了。

生成器類看起來很簡單,其實就是將煎魚在上面寫的簡單生成器寫成一個類。

重點就是,咱們得找到「推導」,推導在這裏是指next函數 —— 咱們實現的生成器類最重要的就是next()。

咱們來實現上面的y函數的生成器類:

class Y(object):

    def __init__(self, n_max):
        self.n_max = n_max
        self.n = 0
        self.y = 0

    def __iter__(self):
        return self

    def next(self):
        if self.n < self.n_max:
            self.y = self.y ** 2 + 1
            self.n += 1
            return self.y
        raise StopIteration()


if __name__ == '__main__':
    y = Y(13)
    for i in y:
        print(i)

有幾點是須要注意的:

  1. 類須要包含__iter__()函數,而返回的不必定是self,可是須要生成器
  2. 實現next方法,「迭代」的邏輯請放到該函數裏面
  3. 若是須要引入別的庫或者寫別的函數,能夠在類中隨便加

接下來,煎魚帶來一段很無聊的表演,來表示__iter__()函數而返回的不必定是self:

class SuperY(object):

    def __init__(self, n_max):
        self.n_max = n_max

    def __iter__(self):
        return Y(self.n_max)

if __name__ == '__main__':
    sy = SuperY(13)
    for i in sy:
        print(i)

這段代碼的輸出和上一段如出一轍。

生成器的照妖鏡

這裏照妖鏡的意思,指一個能鑑別某對象(甚至不是對象)是否一個生成器的東西。

提及來,可能會有點多餘並且零碎。

其中有三個函數:

  1. isgeneratorfunction(),字面意思,是否生成器函數
  2. isgenerator(),仍是字面意思,是否生成器
  3. isinstance(),這個指某對象是否爲某個類的實例

咱們把前面寫過的y(帶yield的函數),和Y(生成器類)導入後,進行實驗觀察:

from use_yield import y_with_yield as y
from iter_obj import Y
from inspect import isgeneratorfunction, isgenerator
from types import GeneratorType
from collections import Iterator

if __name__ == '__main__':
    print(isgeneratorfunction(y))  # True
    print(isgeneratorfunction(Y))  # False
    print(isgeneratorfunction(y(5)))  # False
    print(isgeneratorfunction(Y(5)))  # False
    print(isgenerator(y))  # False
    print(isgenerator(Y))  # False
    print(isgenerator(y(5)))  # True
    print(isgenerator(Y(5)))  # False

    print("")

    print(isinstance(y, GeneratorType))  # False
    print(isinstance(y(5), GeneratorType))  # True
    print(isinstance(Y, GeneratorType))  # False
    print(isinstance(Y(5), GeneratorType))  # False

    print("")

    print(isinstance(y, Iterator))  # False
    print(isinstance(y(5), Iterator))  # True
    print(isinstance(Y, Iterator))  # False
    print(isinstance(Y(5), Iterator))  # True

實驗的結論爲:

  1. 帶yield的y函數是一個生成器函數,帶yield的y函數帶上參數5後,稱爲了生成器。由於y是函數的引用,而帶上了參數5後,y(5)就再也不是函數了,而是經過yield進化成了生成器。
  2. 生成器是類GeneratorType的實例,但很遺憾的是,生成器類的實例不是生成器(黑人問號)。
  3. 然而,生成器y(5)和生成器類的實例都屬於迭代器。

其餘亂七八糟的

Python裏面,range和xrange有什麼不一樣,用哪一個更好,爲何?

對的,就是和生成器有關係,嘿嘿。

先這樣吧

如有錯誤之處請指出,更多地請關注造殼

相關文章
相關標籤/搜索