摘要:為什么是斐波那契談到生成器迭代器,人們總是喜歡用斐波那契數列來舉例。那么,換句話來說,即能由推導式得出的數列,其實都可以用來做生成器迭代器的例子。然而,生成器和生成器類的實例都屬于迭代器。
“Python有什么好學的”這句話可不是反問句,而是問句哦。
主要是煎魚覺得太多的人覺得Python的語法較為簡單,寫出來的代碼只要符合邏輯,不需要太多的學習即可,即可從一門其他語言跳來用Python寫(當然這樣是好事,誰都希望入門簡單)。
于是我便記錄一下,如果要學Python的話,到底有什么好學的。記錄一下Python有什么值得學的,對比其他語言有什么特別的地方,有什么樣的代碼寫出來更Pythonic。一路回味,一路學習。
為什么是斐波那契談到生成器/迭代器,人們總是喜歡用斐波那契數列來舉例。
斐波那契數列,數學表示為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。
即,斐波那契數是可以通過推導式得出的。
就是這樣一種類似于冥冥注定的感覺:當前的平行空間,是由你之前的選擇決定的。而且是欽定好的,你接下來的每一步,其實都已經被決定了,由什么決定呢,由你以前走過的路決定的。
那么,換句話來說,即能由推導式得出的數列,其實都可以用來做生成器/迭代器的例子。例如,煎魚用一條式子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般的獰笑):
暴露n的值,n值可以作為參數輸入,即我需要控制數列計算到哪
函數返回(輸出)整個數列
接下來,函數改成:
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變成這樣,其實就是搞成了一個簡單的生成器。在這里,生成器的作用有:
for i in y_with_yield(13000000)的循環中,每一次循環程序才會進入函數去計算,而不會把全部結果都計算出來再返回
由于不會把全部結果都計算出來再返回,程序運行所需的內存也大幅地減少
暫時給出初步結論:
這個小生成器在較大數據的計算量時,有較大的優勢
程序把推導式的計算通過yield分散了,降低了cpu和內存的壓力
如果沒有煎魚的需求,這一切都白搭且多余
我們再來看下生成器的其他用途吧。
通過緩存機制讀取文件在讀文件或處理文件時使用緩存是很有必要的,因為我們總是不知道文件會有多大,文件的大小會不會把程序給拖垮。
煎魚新建一個文件(假設叫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)
有幾點是需要注意的:
類需要包含__iter__()函數,而返回的不一定是self,但是需要生成器
實現next方法,“迭代”的邏輯請放到該函數里面
如果需要引入別的庫或者寫別的函數,可以在類中隨便加
接下來,煎魚帶來一段很無聊的表演,來表示__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)
這段代碼的輸出和上一段一模一樣。
生成器的照妖鏡這里照妖鏡的意思,指一個能鑒別某對象(甚至不是對象)是否一個生成器的東西。
說起來,可能會有點多余而且零碎。
其中有三個函數:
isgeneratorfunction(),字面意思,是否生成器函數
isgenerator(),還是字面意思,是否生成器
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
實驗的結論為:
帶yield的y函數是一個生成器函數,帶yield的y函數帶上參數5后,稱為了生成器。因為y是函數的引用,而帶上了參數5后,y(5)就不再是函數了,而是通過yield進化成了生成器。
生成器是類GeneratorType的實例,但很遺憾的是,生成器類的實例不是生成器(黑人問號)。
然而,生成器y(5)和生成器類的實例都屬于迭代器。
其他亂七八糟的Python里面,range和xrange有什么不同,用哪個更好,為什么?
對的,就是和生成器有關系,嘿嘿。
先這樣吧
若有錯誤之處請指出,更多地請關注造殼。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/44954.html
摘要:然后煎魚加了一個后再調用函數,得到的輸出結果和加修飾器的一樣,換言之等效于因此,我們對于,可以理解是,它通過閉包的方式把新函數的引用賦值給了原來函數的引用。 Python有什么好學的這句話可不是反問句,而是問句哦。 主要是煎魚覺得太多的人覺得Python的語法較為簡單,寫出來的代碼只要符合邏輯,不需要太多的學習即可,即可從一門其他語言跳來用Python寫(當然這樣是好事,誰都希望入門簡...
摘要:引上下文管理器太極生兩儀,兩儀為陰陽。而最常用的則是,即上下文管理器使用上下文管理器用之后的文件讀寫會變成我們看到用了之后,代碼沒有了創建,也沒有了釋放。實現上下文管理器我們先感性地對進行猜測。現實一個上下文管理器就是這么簡單。 Python有什么好學的這句話可不是反問句,而是問句哦。 主要是煎魚覺得太多的人覺得Python的語法較為簡單,寫出來的代碼只要符合邏輯,不需要太多的學習即可...
摘要:抓住了迭代器模式的本質,即是迭代,賦予了它極高的地位。輸出結果輸出結果小結迭代器模式幾乎是種設計模式中最常用的設計模式,本文主要介紹了是如何運用迭代器模式,并介紹了模塊生成迭代器的種方法,以及種生成迭代器的內置方法。 showImg(https://segmentfault.com/img/bVbmv7W?w=4272&h=2848); 在軟件開發領域中,人們經常會用到這一個概念——設...
閱讀 3324·2021-11-23 09:51
閱讀 2449·2021-11-09 09:46
閱讀 1482·2019-08-30 15:54
閱讀 3129·2019-08-30 14:22
閱讀 2914·2019-08-29 12:40
閱讀 1636·2019-08-26 10:33
閱讀 1784·2019-08-23 17:09
閱讀 1560·2019-08-23 16:11