標籤: generator

[Effective Python] 情境:考慮使用產生器而非回傳串列

[Effective Python] 情境:考慮使用產生器而非回傳串列

考慮一種情境,在python中,我們常常會設計一些function來查找串列中match的資料。

例如以下的程式,我們希望找出int串列中,符合特定值的所在位置,若我們按照傳統的寫法,最簡單的就是走訪所有的項目,逐一比對後,再append到result的串列中(看你想放什麼,可以是index或對應的object)

def find_matched_number_location(number, numbers):
    result = []
    if numbers:
        for index, number_in_list in enumerate(numbers):
            if number_in_list == number:
                result.append(index+1)

    return result



result = find_matched_number_location(3, [1, 2, 4, 5, 2, 3, 12, 3, 5, 7, 1])
print(result)

對於某些輸入的樣本來說,這樣能如期的運作

[6, 8]

不過這種函式有兩個問題存在(Effective Python, P41建議)

第1個問題是,這種程式碼有點過於密集,且帶有雜訊。每次符合的條件滿足後,就會呼叫一次append。這樣的呼叫體積大,其中又用了某一行建立結果串列,再一行來return它

第2個問題是,在回傳之前,它對將所有的結果儲存在串列中才行。這對於超大型輸入,可能會使得我們程式耗盡記憶體而當掉。相交之下,這種函式的改成generator版本

能夠輕易地處理任意長度的輸入。

 

撰寫這種函式比較好的方式是使用generator(產生器)。產生器是使用了yield運算式的函式。被呼叫時,產生器函式實際上還不會執行,而是立即回傳一個iterator(迭代器)

。每次呼叫next()函式時,這個iterator會將產生器到它的下一個yield運算式。因此我們來改寫這個find_matched_number_location函式吧

結果如下:

def find_matched_number_location_new(number, numbers):
    if numbers:
        for index, number_in_list in enumerate(numbers):
            if number_in_list == number:
                yield index + 1

result_new = list(find_matched_number_location_new(3, [1, 2, 4, 5, 2, 3, 12, 3, 5, 7, 1]))
print(result_new)

帶來的好處就是減少了與結果變數互動的所有地方。呼叫generator所回傳的iterator可輕易地被轉成一個串列。

 

為了突顯第2個問題,我建立了一個3MB的檔案,裡面盡是數字用逗點隔開

在此我定義了一個generator,它會從檔案接受逐行的串流輸入(不過此例只有一行),一次只產出一個比對數字的輸出。這個函式工作時的最大記憶體量,僅會是單一行輸入的最大長度

def find_matched_number_location_from_file(number, handle):
    offset = 0
    for line in handle:
        if line:
            for number_in_file in line.split(','):
                offset += 1
                if int(number_in_file) == number:
                    yield offset

執行2段的測試

start = time.time()
with open('新增資料夾/test_data.txt', 'r') as f:
    it = find_matched_number_location_from_file(3, f)
    result = list(it)
    print(len(result))
end = time.time()
print( '花費 %.3f 秒' % (end - start) )

start = time.time()
with open('新增資料夾/test_data.txt', 'r') as f:
    result = []
    for line in f:
        for index , number_in_file in enumerate(line.split( ',' )):
            if int(number_in_file) == 3:
                result.append(index + 1)
    print(len(result))
end = time.time()
print('花費 %.3f 秒' % (end - start))

執行結果如下:

307824
花費 0.551
307824
花費 0.730 秒

其實對於我在測試資料僅僅只有3MB的檔案之下,其實秒數落差不大,但是效能上還是有所區別,generator的方法比舊的比對方法快了約25%。而佔的記憶體量,舊的比對方法(用result來記錄)則會多用了了至少307824個數字所站的位元。這邊數字或許不大,但試想若是大型的資料的情況呢?就交由使用場景來決定吧。

最後,再補充一點,定義像這樣的產生器時,唯一要注意的部份就是呼叫者必須知道期回傳的iterator是有狀態的,不能被重複使用。

下篇來分享防備的做法

 

Reference:Effective Python中文版一書,做法16,改為自己的理解與實作