原文轉載自「劉悅的技術博客」v3u.cn/a_id_97python
最近不管是面試仍是筆試,有一個高頻問題始終陰魂不散,那就是給一個大文件,至少超過10g,在內存有限的狀況下(低於2g),該以什麼姿式讀它?面試
全部人都知道,用python讀文件有一套」標準流程「:編程
def retrun_count(fname):
"""計算文件有多少行 """
count = 0
with open(fname) as file:
for line in file:
count += 1
return count
複製代碼
爲何這種文件讀取方式會成爲標準?這是由於它有兩個好處:bash
with 上下文管理器會自動關閉打開的文件描述符
在迭代文件對象時,內容是一行一行返回的,不會佔用太多內存編程語言
但這套標準作法並不是沒有缺點。若是被讀取的文件裏,根本就沒有任何換行符,那麼上面的第二個好處就不成立了。當代碼執行到 for line in file 時,line 將會變成一個很是巨大的字符串對象,消耗掉很是可觀的內存。函數
若是有一個 5GB 大的文件 big_file.txt,它裏面裝滿了隨機字符串。只不過它存儲內容的方式稍有不一樣,全部的文本都被放在了同一行裏優化
若是咱們繼續使用前面的 return_count 函數去統計這個大文件行數。那麼在一臺pc上,這個過程會足足花掉 65 秒,並在執行過程當中吃掉機器 2GB 內存ui
爲了解決這個問題,咱們須要暫時把這個「標準作法」放到一邊,使用更底層的 file.read() 方法。與直接循環迭代文件對象不一樣,每次調用 file.read(chunk_size) 會直接返回從當前位置日後讀取 chunk_size 大小的文件內容,沒必要等待任何換行符出現。spa
因此,若是使用 file.read() 方法,咱們的函數能夠改寫成這樣:code
def return_count_v2(fname):
count = 0
block_size = 1024 * 8
with open(fname) as fp:
while True:
chunk = fp.read(block_size)
# 當文件沒有更多內容時,read 調用將會返回空字符串 ''
if not chunk:
break
count += 1
return count
複製代碼
在新函數中,咱們使用了一個 while 循環來讀取文件內容,每次最多讀取 8kb 大小,這樣能夠避免以前須要拼接一個巨大字符串的過程,把內存佔用下降很是多。
利用生成器解耦代碼
假如咱們在討論的不是 Python,而是其餘編程語言。那麼能夠說上面的代碼已經很好了。可是若是你認真分析一下 return_count_v2 函數,你會發如今循環體內部,存在着兩個獨立的邏輯:數據生成(read 調用與 chunk 判斷) 與 數據消費。而這兩個獨立邏輯被耦合在了一塊兒。
爲了提高複用能力,咱們能夠定義一個新的 chunked_file_reader 生成器函數,由它來負責全部與「數據生成」相關的邏輯。這樣 return_count_v3 裏面的主循環就只須要負責計數便可。
def chunked_file_reader(fp, block_size=1024 * 8):
"""生成器函數:分塊讀取文件內容 """
while True:
chunk = fp.read(block_size)
# 當文件沒有更多內容時,read 調用將會返回空字符串 ''
if not chunk:
break
yield chunk
def return_count_v3(fname):
count = 0
with open(fname) as fp:
for chunk in chunked_file_reader(fp):
count += 1
return count
複製代碼
進行到這一步,代碼彷佛已經沒有優化的空間了,但其實否則。iter(iterable) 是一個用來構造迭代器的內建函數,但它還有一個更少人知道的用法。當咱們使用 iter(callable, sentinel) 的方式調用它時,會返回一個特殊的對象,迭代它將不斷產生可調用對象 callable 的調用結果,直到結果爲 setinel 時,迭代終止。
def chunked_file_reader(file, block_size=1024 * 8):
"""生成器函數:分塊讀取文件內容,使用 iter 函數 """
# 首先使用 partial(fp.read, block_size) 構造一個新的無需參數的函數
# 循環將不斷返回 fp.read(block_size) 調用結果,直到其爲 '' 時終止
for chunk in iter(partial(file.read, block_size), ''):
yield chunk
複製代碼
最後只須要兩行代碼,就構造出了一個可複用的分塊讀取方法,和一開始的」標準流程「按行讀取 2GB 內存/耗時 65 秒 相比,使用生成器的版本只須要 7MB 內存 / 12 秒就能完成計算。效率提高了接近 4 倍,內存佔用更是不到原來的 1%,簡直完美。
原文轉載自「劉悅的技術博客」 v3u.cn/a_id_97