https://www.toutiao.com/article/7198422215929381376/?log_from=c48d43ad3b0ad8_1676264616195 你是否曾經必須處理一個龐大到使計算機的內存不堪重負的數(shù)據(jù)集,?此時,,需要使用Python中的生成器(Generator)對象。 在本文結束時,,你將知道:
如果你是一個初級或中級的Python用戶,并且你有興趣學習如何以一種更正宗的Python方式處理大型數(shù)據(jù)集,,那么這篇文章就非常適合你。 使用生成器根據(jù)PEP 255,,生成器函數(shù)是一種特殊函數(shù),,它返回一個惰性迭代器對象,這個對象可以像列表一樣進行循環(huán),。但是,,與列表不同,惰性迭代器不會將其內容存儲在內存中?,F(xiàn)在你已經對生成器的功能有了大致的了解,,你可能想知道它們在運行中是什么樣子的。讓我們看兩個例子,。在第一個例子中,,我們將大致了解生成器是如何工作的。然后,,我們對每個示例進行放大并更加徹底地查看,。 示例1:讀取大型文件生成器的一個常見用例是處理數(shù)據(jù)流或大型文件,如CSV文件,,一種使用逗號作為分隔符將數(shù)據(jù)分隔成列的文件,,這種格式是共享數(shù)據(jù)的常見方式。現(xiàn)在,,如果你想統(tǒng)計CSV文件中的行數(shù),,該怎么辦呢?下面的代碼塊顯示了一種方法:
在這個例子中,,你可能希望csv-gen是一個列表,,csv_reader()打開一個文件并將其內容賦值csv_gen。然后,,遍歷列表,,并row_count記錄行數(shù)。 這是一個可執(zhí)行的程序,,但如果文件很大,,這種設計是否仍然有效?如果文件比可用內存大怎么辦,?要回答這一問題,,假設csv_reader()打開文件,,并將其內容讀入:
此函數(shù)打開給定的文件,并使用file.read()和.split()將每一行作為單獨的元素添加到列表中,。如果要在前面的行計數(shù)代碼塊中使用此版本的csv_reader(),,則會得到以下輸出:
在這種情況下,open()返回一個生成器對象,,你可以一行一行地惰性地遍歷該對象,。但是,file.read().split()會立即將所有內容加載到內存中,,從而導致MemoryError,。 在那之前,你可能會注意到你的電腦慢得像爬一樣,,你甚至可能需要用KeyboardInterrupt來終止程序,。那么,如何處理這些龐大的數(shù)據(jù)文件呢,?看看csv_reader()的新定義:
在這個版本中,,打開文件,遍歷它,,并生成一行,。此代碼應生成以下輸出,無內存錯誤:
這里發(fā)生了什么事,?實際上,,你已經將csv_reader()變成了一個生成器函數(shù)。這個版本打開一個文件,,循環(huán)遍歷每一行,,并生成每一行,而不是返回它,。 你還可以定義生成器表達式(也稱為生成器解析式),,該表達式的語法與列表解析非常相似。這樣,,你就可以使用生成器而無需調用函數(shù):
這是創(chuàng)建csv_gen的更簡潔的方法,。你將很快了解Python yield語句的更多信息?,F(xiàn)在,請記住這一關鍵區(qū)別:
示例2:生成無限序列我們來看無限序列的生成,。在Python中,,要獲取有限序列,,可以調用range(),并用list函數(shù)顯示其值:
但是,生成無限序列需要使用生成器,,因為你的計算機內存是有限的:
這個代碼塊又短又甜,。首先,初始化變量num并開始無限循環(huán),。然后,,用yield num,循環(huán)掛起,,返回此時的值,。 在yield語句之后,將num增加1,。如果你嘗試使用for循環(huán),,將看到它確實看起來是無限的:
程序將繼續(xù)執(zhí)行,直到你手動停止它,。 你也可以直接對生成器對象調用next(),而不是使用for循環(huán),,這對于在交互模式中測試生成器特別有用:
這里,,有一個名為gen的生成器,你可以通過重復調用next()來手動遍歷它,。這是一個很好的完備性檢查,,以確保生成器正在生成你所期望的輸出。 注意:當使用next()時,,Python將對作為參數(shù)傳入的函數(shù)調用.__next__(),。這個參數(shù)化允許一些特殊效果,但它超出了本文要討論的范圍,。嘗試更改傳遞給next()的參數(shù),,看看會發(fā)生什么! 示例3:檢測回文你可以在許多方面使用無限序列,,但它們的一個實際用途是構建回文檢測器,。回文檢測器將定位回文中的所有字母序列或數(shù)字序列,。這些單詞或數(shù)字向前讀取和向后讀取時是相同的,,如121。首先,,定義數(shù)字回文檢測器:
不要太擔心對于這段代碼中的數(shù)學原理的理解,。只需注意,該函數(shù)接受一個輸入數(shù)字,,將其反轉,,并檢查反轉的數(shù)字是否與原始數(shù)字相同。現(xiàn)在,你可以使用無限序列生成器查找獲取所有數(shù)字回文:
在這種情況下,,打印出來的數(shù)字是那些向前讀或向后讀都相同的數(shù)字,。 注意:實際上,你沒有必要自己編寫無限序列生成器,,itertools模塊使用itertools.count()提供了非常高效的無限序列生成器,。 現(xiàn)在你已經看到了無限序列生成器的一個簡單用例,下面,,讓我們深入了解生成器的工作原理,。 了解生成器到目前為止,你已經了解了創(chuàng)建生成器的兩種主要方法:使用生成器函數(shù)和生成器表達式,。你甚至可以直觀地了解生成器是如何運行的,。讓我們花點時間把這些知識說得更清楚一些。 生成器函數(shù)的外觀和行為與常規(guī)函數(shù)類似,,但有一個定義特征,。生成器函數(shù)使用Python yield關鍵字而不是return?;叵胍幌履阒熬帉懙纳善骱瘮?shù):
除了yield語句及其后面的代碼,,這看起來像一個典型的函數(shù)定義。yield表示在哪個位置將值返回給調用者,。與return不同的是,,你不會在隨后退出該函數(shù)。 相反,,函數(shù)的狀態(tài)會被記住,。這樣,當對生成器對象調用next()時(在for循環(huán)中顯式或隱式調用),,先前生成的變量num將遞增,,然后再次生成變量。由于生成器函數(shù)看起來像其他函數(shù),,并且與它們的行為非常相似,,因此可以假設生成器表達式與Python中提供的其他解析式非常相似。 使用生成器表達式創(chuàng)建生成器與列表解析一樣,,生成器表達式允許你在幾行代碼中快速創(chuàng)建生成器對象,。生成器表達式在使用列表解析的情況下也很有用。還有一個額外的好處:你可以在迭代之前創(chuàng)建它們,,而無需在內存中構建和保存整個對象,。換句話說,當你使用生成器表達式時,,將不會有內存損失,。舉一個對一些數(shù)字求平方的例子:
nums_squared_lc和nums_squared_gc看起來基本相同,但有一個關鍵區(qū)別。你能看出來嗎,?看一看查看這些對象時發(fā)生的情況:
第一個對象使用括號來構建列表,,而第二個對象使用括號創(chuàng)建生成器表達式,輸出結果顯示你已經創(chuàng)建了一個生成器對象,,并且它與列表不同,。 生成器的性能你之前了解到,生成器是優(yōu)化內存的一種好方法,。雖然無限序列生成器是這種優(yōu)化的一個極端示例,,但讓我們放大剛才看到的數(shù)字平方示例,并檢查結果對象的大小,??梢酝ㄟ^調用sys.getsizeof()來完成此操作:
在這種情況下,從列表解析中得到的列表是87624字節(jié),,而生成器對象只有120字節(jié),。這意味著列表比生成器對象大700多倍! 不過,,有一件事要記住,。如果列表小于正在運行的計算機的可用內存,則列表解析的計算速度可能比等效的生成器表達式更快,。為了探究這個問題,我們來總結一下以上兩種解析的結果,??梢允褂胏Profile.run()生成讀數(shù):
在這里,你可以看到列表解析中所有值的求和大約花費了生成器中求和的三分之一時間,。如果問題在于速度而不在于內存,,那么列表解析可能是更好的工具。 注意:這些度量不僅對使用生成器表達式生成的對象有效,。對于由類似生成器函數(shù)生成的對象,,它們也是相同的,因為用來生成對象的生成器是等效的,。 記住,,列表解析返回完整列表,而生成器表達式返回生成器,。生成器的工作原理是一樣的,,無論它們是通過函數(shù)還是表達式來構建的。使用表達式只允許你在一行中定義簡單的生成器,,在每次內部迭代結束時都會有一個假定的結果,。 Python yield語句無疑是生成器所有功能的關鍵所在,因此讓我們深入了解一下yield在Python中的工作原理。 理解Yield語句總的來說,,yield語句是一個相當簡單的語句,。它的主要工作是以類似于return語句的方式控制生成器函數(shù)的流。不過,,如上所述,,Python yield語句有一些技巧。 當調用生成器函數(shù)或使用生成器表達式時,,將返回一個叫做生成器的特殊迭代器,。你可以把這個生成器分配給變量以便使用它。在生成器上調用特殊方法(如next())時,,函數(shù)中的代碼將一直執(zhí)行到y(tǒng)ield,。 當遇到y(tǒng)ield語句時,程序將暫停函數(shù)執(zhí)行并將生成的值返回給調用方(相反,,return完全停止函數(shù)執(zhí)行),,當函數(shù)暫停時,保存該函數(shù)的狀態(tài),。這包括生成器本地的任何變量綁定,、指令指針、內部堆棧和任何異常處理,。 這允許你在調用生成器的某個方法時恢復函數(shù)執(zhí)行,。這樣,所有的功能在yield之后馬上恢復,。通過使用多個Python yield語句,,你可以看到這一點:
仔細看一下最后對next()的調用。你可以看到執(zhí)行過程中出現(xiàn)了異常,,這是因為生成器和所有迭代器一樣,,都可能耗盡。除非生成器是無限的,,否則只能迭代一次,。一旦所有的值都被返回,迭代將停止,,使用for循環(huán)則會退出,;使用next(),則會得到顯式的StopIteration異常,。 注意:StopIteration是一個自然的異常,,它被用來表示迭代器的結束。例如,,for循環(huán)是圍繞StopIteration構建的,。你甚至可以使用while循環(huán)來實現(xiàn)你自己的for循環(huán):
yield可以通過多種方式控制生成器的流,。只要你有足夠的創(chuàng)造力,就可以使用多個Python yield語句,。 使用先進的生成器方法你已經看到了生成器最常見的用途和構造,,但還有一些技巧需要介紹。除了yield之外,,生成器對象還可以使用以下方法:
怎樣使用.send()對于下面的內容,,你將構建一個使用這三種方法的程序。這個程序將像以前一樣打印回文數(shù)字,,但有一些微調,。遇到回文后,新程序將添加一個數(shù)字并從那里開始搜索下一個數(shù)字,。還要使用.throw()處理異常,,并使用.close()在指定數(shù)量的數(shù)字后停止生成器。首先,,讓我們回憶一下回文檢測器的代碼:
這與前面看到的代碼相同,,只是現(xiàn)在程序嚴格地返回True或False。你還需要修改原來的無限序列生成器,,如下所示:
這里有很多變化,!第一個是在第5行,其中i = (yield num),。雖然你之前知道了yield是一個語句,,但這并不是全部。 從Python2.5(這個版本引入了你現(xiàn)在正在學習的方法)開始,,yield是一個表達式,,而不是一個語句。當然,,你仍然可以用它作為語句。但是現(xiàn)在,,你也可以按照在上面的代碼塊中所看到的那樣使用它,,其中i接受生成的值。這樣,,你就可以對生成的值進行操作,。更重要的是,你可以用.send()將值返回到生成器,。當在yield之后執(zhí)行時,,i將接受返回的值。 再看if i is not None,,如果對生成器對象調用next(),,則可能發(fā)生這種情況(當你使用for循環(huán)迭代時也可能發(fā)生這種情況),,如果i有一個值,則使用新值更新num,,但無論i值是否為空,,你都將增加num并再次啟動循環(huán)。 現(xiàn)在,,看看主函數(shù)代碼,,它把測量的長度值發(fā)回生成器。例如,,如果回文是121,,那么它將.send()1000:
通過這段代碼,您可以創(chuàng)建生成器對象并遍歷它,。程序只在找到回文后才產生一個值,。它使用len()來確定回文中的數(shù)字位數(shù)。然后,,它向生成器發(fā)送10 ** digits,。這使執(zhí)行返回到生成器邏輯中,將10 ** digits分配給i,。因為i現(xiàn)在有一個值,,所以程序會更新num,遞增,、并再次檢查回文,。 一旦代碼找到并生成另一個回文,你將通過for循環(huán)進行迭代,。這與使用next()進行迭代相同,。生成器還在第5行i = (yield num)處開始工作。但是,,現(xiàn)在i是None,,因為你沒有顯式地發(fā)送值。 你在這里創(chuàng)建的是一個協(xié)程(coroutine),,或者一個可以向其傳數(shù)據(jù)的生成器函數(shù),。這些對于構建數(shù)據(jù)管道是有用的,但是正如你很快就將看到的,,它們對于構建數(shù)據(jù)管道并不是必須的。 現(xiàn)在你已經了解了.send(),,再來看一看.throw()。 怎樣使用.throw().throw()允許你拋出生成器的異常,。在下面的示例中,,你將在第6行中引發(fā)異常,。當digits達到5時,此代碼將拋出ValueError:
這與前面的代碼相同,,但現(xiàn)在你將檢查digits是否等于5。如果是,,那么.throw()異常ValueError。要確認代碼是否如期運行,,請查看代碼的輸出:
.throw()在需要捕獲異常的任何區(qū)域都很有用,。在本例中,你使用.throw()來控制何時停止遍歷生成器,。使用.close(),你可以更優(yōu)雅地執(zhí)行此操作,。 怎樣使用.close()顧名思義,,.close()允許你停止生成器。這在控制無限序列生成器時特別方便,。我們通過將.throw()更改為.close()來更新上面的代碼,以停止迭代:
不要調用.throw(),,而是在第6行中使用.close()。使用.close()的優(yōu)點是它引發(fā)了StopIteration,,這是一個用于表示有限迭代器結束的異常:
生成器附帶了一些特殊方法。現(xiàn)在,,你已經了解了關于這些特殊方法的更多信息,。接下來,我們討論一下如何使用生成器來創(chuàng)建數(shù)據(jù)管道,。 使用生成器創(chuàng)建數(shù)據(jù)管道數(shù)據(jù)管道允許你將代碼串在一起,,以處理大型數(shù)據(jù)集或數(shù)據(jù)流,,而不必占用機器的內存。假設你有一個大型CSV文件:
此示例來自TechCrunch Continental USA,,該集合描述了在美國的各種初創(chuàng)企業(yè)的融資輪次和美元金額,。 是時候用Python做一些大事情了!為了演示如何使用生成器構建管道,,你將分析此文件,,以獲取數(shù)據(jù)集中所有A輪融資的總值和平均值,。 我們來設計一個策略:
通常,你可以使用Pandas完成這項工作,,但也可以僅使用幾個生成器來實現(xiàn)這項功能,。首先,,使用生成器表達式來讀取文件中的每一行:
然后,,使用與前一個表達式一致的另一個生成器表達式,將每一行拆分為一個列表:
在這里,,你創(chuàng)建了生成器list_line,,它遍歷第一個生成器lines。這是設計生成器管道時使用的常見模式,。接下來,,從techcrunch.csv中提取列名。由于列名往往在CSV文件的第一行,,因此可以通過一個簡短的next()獲取該行:
調用next(),,使迭代器在list_line生成器上前進了一步,。把它們放在一起,,代碼應該如下所示:
總而言之,,首先創(chuàng)建一個生成器表達式lines,,以生成文件中的每一行。接下來,,在另一個名為list_line的生成器表達式的遍歷該生成器,;list_line將每一行轉換為一個列表。然后,,使用next()把list_line迭代器推進一步,以便從CSV文件中獲取列名的列表,。 注意:當心后面的換行符,!此代碼利用list_line生成器表達式中的.rstrip(),以確保CSV文件中不存在尾隨的換行符,。 為了便于篩選和執(zhí)行數(shù)據(jù)操作,你可以創(chuàng)建字典,,其中的鍵是CSV中的列名:
此生成器表達式遍歷list_line,使用zip()和dict()創(chuàng)建上面指定的字典?,F(xiàn)在,你將使用第4個生成器篩選所要的融資輪次,,并將raisedAmt拉出來:
在這個代碼片段中,,生成器表達式遍歷company_dicts,并獲取滿足條件company_dict["round"] == "a"的值company_dict["raisedAmt"],。 記住,在生成器表達式中并不是一次遍歷所有這些內容,。實際上,,在真正使用for循環(huán)或用于迭代器的函數(shù)(如sum())之前,,不會遍歷任何內容,。我們現(xiàn)在調用sum()來遍歷生成器:
把這些放在一起,,你將產生以下腳本:
這個腳本將你構建的每個生成器組合在一起,,它們都作為一個大數(shù)據(jù)管道運行,。下面是逐行分解:
當你對數(shù)據(jù)集techcrunch.csv執(zhí)行此代碼時,,會發(fā)現(xiàn)在A輪融資中總共籌集了4376015000美元,。 注意:本文中開發(fā)的處理CSV文件的方法對于理解如何使用生成器和Python yield語句非常重要,。但是,,在Python中使用CSV文件時,,應該使用Python標準庫中包含的CSV模塊,。此模塊優(yōu)化了有效處理CSV文件的方法,。 為了更深入地挖掘,,試著計算出每家公司在A輪融資中的平均融資額,。這有點棘手,,所以這里有一些提示:
結論你學習了生成器函數(shù)和生成器表達式,。 |
|
來自: 山峰云繞 > 《Python代碼知識游戲黑客編程與英語》