考慮一種情境,在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,改為自己的理解與實作
One Reply to “[Effective Python] 情境:考慮使用產生器而非回傳串列”
這本書也有入手~ 但一直沒去翻他XD