
文章目錄環境信息前言800MB的CSV文件直接 read 出來內存爆了一、生成器原理yield 到底做了什么二、生成器表達式一行寫法的陷阱三、實戰場景1流式讀取大CSV四、實戰場景2流式解析大JSON五、實戰場景3API分頁拉取六、量化驗證生成器到底省了多少內存七、三個使用邊界八、總結環境信息項目版本/說明Python3.10標準庫csv / json / itertools無需第三方內存監控tracemalloc標準庫測試場景大CSV文件 / 大JSON文件 / 分頁API前言800MB的CSV文件直接 read 出來內存爆了上個月我處理香港政府開放數據平臺data.gov.hk的一個交通流量數據集——800MB 的 CSV幾百萬行記錄。我的第一版代碼是這樣的importcsv# ? 危險寫法一次性把所有行讀進內存withopen(traffic_flow_800mb.csv,r,encodingutf-8)asf:rowslist(csv.reader(f))# 800MB 數據全部加載進內存print(f加載了{len(rows)}行)# 然后內存就爆了——list 里存了幾百萬個 list 對象實際內存占用遠超 800MB跑起來之后機器直接卡死。因為list(csv.reader(f))會把每一行都轉換成一個 Python list 對象幾百萬個 list 的開銷疊加起來實際內存占用是文件大小的好幾倍。后來我把代碼改成了生成器寫法importcsv# ? 生成器寫法逐行處理內存占用恒定defread_csv_rows(filepath):withopen(filepath,r,encodingutf-8)asf:readercsv.reader(f)forrowinreader:yieldrow# 每次只返回一行不積累fori,rowinenumerate(read_csv_rows(traffic_flow_800mb.csv)):# 處理這一行...ifi%1000000:print(f已處理{i}行)同樣的功能內存占用從文件大小的數倍降到恒定的幾十KB。這就是生成器的力量。收藏提示①處理大文件的第一原則——永遠不要list()一個文件讀取器。用yield逐行返回內存占用恒定。文末有3個開箱即用的生成器實戰。一、生成器原理yield到底做了什么要理解為什么生成器省內存先理解它和普通函數、普通列表的區別。普通函數調用后一次性執行完用return返回結果然后函數就結束了。生成器函數函數體里有yield關鍵字。調用它不會立即執行函數體而是返回一個生成器對象。每次對這個生成器調用next()函數才執行到下一個yield處把yield后面的值吐出來然后暫停。defsimple_generator():print(開始)yield1# 第一次 next() 執行到這里返回1暫停print(繼續)yield2# 第二次 next() 執行到這里返回2暫停print(結束)# 第三次 next() 會拋出 StopIterationgensimple_generator()print(next(gen))# 輸出開始然后返回 1print(next(gen))# 輸出繼續然后返回 2print(next(gen))# 輸出結束然后拋出 StopIteration關鍵就在于暫停這兩個字。生成器不會一次性把所有值算出來存著而是用到哪個算哪個。這就是惰性求值lazy evaluation。對比一下# 列表一次性生成100萬個數字全部存內存nums_list[iforiinrange(1000000)]# 內存占用約 36MB每個int對象28字節 × 100萬# 生成器只是定義了一個規則不實際生成nums_gen(iforiinrange(1000000))# 內存占用約 100字節就一個生成器對象同樣是 100 萬個數字列表占 36MB生成器只占 100 字節。這就是內存直降 90%“的真正來源——不是魔法是不預先計算、用到才算”。二、生成器表達式一行寫法的陷阱注意上面nums_gen (i for i in range(1000000))用的是圓括號這是生成器表達式。而列表推導式用的是方括號。# 列表推導式方括號—— 立即生成占內存squares_list[x*xforxinrange(1000000)]# 生成器表達式圓括號—— 惰性不占內存squares_gen(x*xforxinrange(1000000))一個常見的坑生成器表達式只能迭代一次。因為它不是存著結果而是邊算邊吐吐完就沒了gen(x*xforxinrange(5))print(list(gen))# [0, 1, 4, 9, 16]print(list(gen))# [] —— 已經耗盡第二次是空的如果你需要多次遍歷要么用列表要么用itertools.tee要么重新創建生成器。三、實戰場景1流式讀取大CSV回到開頭的大文件場景把流式讀取封裝成一個可復用的生成器importcsvdefstream_csv(filepath,skip_headerTrue): 流式讀取大CSV逐行yield不占內存 skip_header: 是否跳過表頭行 withopen(filepath,r,encodingutf-8)asf:readercsv.DictReader(f)# DictReader 返回字典鍵是表頭forrowinreader:yieldrow# 使用逐行處理配合條件過濾forrowinstream_csv(traffic_flow_800mb.csv):# 只處理屯門區的數據ifrow.get(district)屯門:process(row)如果還要做過濾 轉換可以鏈式組合生成器deffilter_by_district(rows,district):過濾生成器只保留指定區forrowinrows:ifrow.get(district)district:yieldrowdefextract_columns(rows,columns):轉換生成器只提取需要的列forrowinrows:yield{col:row.get(col)forcolincolumns}# 鏈式組合三個生成器串成流水線all_rowsstream_csv(traffic_flow_800mb.csv)tuen_mun_rowsfilter_by_district(all_rows,屯門)cleaned_rowsextract_columns(tuen_mun_rows,[time,flow,station])forrowincleaned_rows:print(row)這就是生成器流水線——每一步都是惰性的數據像水流一樣從上一個生成器流到下一個全程不積累。無論文件多大內存占用恒定。四、實戰場景2流式解析大JSONJSON 比 CSV 麻煩因為標準庫的json.load()會把整個文件讀進內存。但可以用ijson庫第三方或者逐條解析 JSON Lines 格式importjsondefstream_jsonl(filepath): 流式讀取 JSON Lines 格式每行一個JSON對象 香港政府部分API返回的就是這種格式 withopen(filepath,r,encodingutf-8)asf:forlineinf:lineline.strip()ifline:# 跳過空行yieldjson.loads(line)# 使用forrecordinstream_jsonl(records.jsonl):ifrecord.get(type)transaction:process(record)收藏提示②如果遇到的是單個超大 JSON 數組[{...},{...},...]標準庫json.load()會全量加載。要么讓數據提供方改 JSON Lines 格式要么用ijson庫做增量解析。JSON Lines 是流式處理的友好格式設計數據管道時優先選它。五、實戰場景3API分頁拉取調用分頁 API 時生成器可以優雅地隱藏分頁邏輯importrequestsdeffetch_paginated(url,page_size100,max_pagesNone): 分頁拉取API數據逐頁yield對外表現為一個連續的流 page1whileTrue:ifmax_pagesandpagemax_pages:breakresprequests.get(url,params{page:page,size:page_size})dataresp.json()recordsdata.get(records,[])ifnotrecords:# 沒有更多數據breakforrecordinrecords:yieldrecord page1# 使用調用方完全不用關心分頁邏輯forrecordinfetch_paginated(https://api.data.gov.hk/v1/records):process(record)六、量化驗證生成器到底省了多少內存口說無憑用tracemalloc實測一下標準庫自帶的內存追蹤工具importtracemallocdefprocess_list(n1000000):列表方式全量加載tracemalloc.start()data[i*2foriinrange(n)]totalsum(data)current,peaktracemalloc.get_traced_memory()tracemalloc.stop()returnpeak/1024/1024# 轉MBdefprocess_generator(n1000000):生成器方式惰性處理tracemalloc.start()totalsum(i*2foriinrange(n))# 注意這是生成器表達式current,peaktracemalloc.get_traced_memory()tracemalloc.stop()returnpeak/1024/1024list_mbprocess_list()gen_mbprocess_generator()print(f列表方式峰值內存:{list_mb:.1f}MB)print(f生成器方式峰值內存:{gen_mb:.1f}MB)print(f內存節省:{(1-gen_mb/list_mb)*100:.0f}%)輸出示例列表方式峰值內存: 36.2 MB 生成器方式峰值內存: 0.1 MB 內存節省: 99%收藏提示③生成器不是更快而是更省內存。在CPU上生成器因為惰性調用有時反而略慢。它的核心價值是——讓你能處理大到放不進內存的數據。如果數據量小到能全放內存列表反而更簡單直接。七、三個使用邊界生成器不是銀彈有三個場景要謹慎需要隨機訪問生成器只能順序迭代不能data[100]這樣隨機取。如果需要反復隨機訪問還是用列表。需要多次遍歷生成器耗盡就沒了。如果需要遍歷兩遍要么存成列表要么用itertools.tee要么重建生成器。數據量小如果數據只有幾百條生成器和列表沒區別反而增加了代碼復雜度。大文件才值得用生成器小數據直接用列表。八、總結生成器的核心就一句話惰性求值——用到哪個算哪個不預先全部算出來。原理yield讓函數暫停而不是結束配合next()逐步取值寫法生成器函數yield和生成器表達式圓括號兩種價值處理大文件/大JSON/分頁API時內存從文件大小數倍降到恒定幾十KB邊界不能隨機訪問、只能遍歷一次、小數據沒必要用這篇是 Python 進階系列的第三篇——0814 寫了正則和裝飾器這篇寫生成器。三個主題的共同點都是寫了很久 Python 但可能沒真正理解的基礎進階。正則解決格式校驗裝飾器解決代碼復用生成器解決內存瓶頸。本文為 Python 生成器技術分享。內存對比數據通過 tracemalloc 實測不同環境數值略有差異但生成器省內存的結論是確定的。香港政府數據示例為演示場景實際數據集以 data.gov.hk 為準。