Python花式讀取大文件(10g/50g/1t)遇到的性能問題(面試向)

原文轉載自「劉悅的技術博客」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

相關文章
相關標籤/搜索