Skip to content

Python Generator:減少記憶體消耗

在 Python 編程中,我們經常需要處理序列數據。但當數據量非常龐大時,一次性將所有數據載入記憶體可能會導致效能問題,甚至 MemoryError。這時候,Python 的生成器 (Generator) 就成了一個強大的工具。本文將帶你深入了解生成器的概念、如何建立和使用它們,以及它們如何幫助我們編寫更高效、更節省記憶體的程式碼。

😮 為什麼我們需要生成器?記憶體的挑戰

想像一下,你需要處理一個包含一百萬個數字的列表,並對每個數字進行平方運算。傳統的做法可能是:

python
def generate_squares_list(n):
    squares = []
    for i in range(n):
        squares.append(i * i)
    return squares

# 處理一百萬個數字
# my_squares = generate_squares_list(1000000) # 這可能會消耗大量記憶體!
# for square in my_squares:
#     # process square
#     pass

如果 n 非常大,squares 列表將會佔用大量記憶體。如果記憶體不足,程式就可能崩潰。生成器提供了一種解決方案,它允許你逐個「產生」值,而不是一次性建立整個序列。

✨ 什麼是生成器 (Generator)?

生成器是一種特殊的迭代器 (Iterator)。你可能已經熟悉迭代器了,例如列表、字串、字典等都是可迭代物件 (Iterable),可以透過 iter() 函數取得其迭代器,然後用 next() 函數逐個取出元素。

生成器的核心特性是惰性求值 (Lazy Evaluation) 或稱 延遲評估:它不會立即計算並儲存所有值,而是在你請求下一個值時才計算它。這樣做的好處是顯而易見的——極大地節省了記憶體,特別是在處理大型數據集或無限序列時。

一個函數如果包含了 yield 關鍵字,它就變成了一個生成器函數。調用生成器函數時,它不會立即執行函數體內的程式碼,而是返回一個生成器物件。

🛠️ 如何建立生成器?

建立生成器主要有兩種方式:

1. 生成器函數 (Generator Functions) 與 yield 關鍵字

這是最常見的建立生成器的方式。當你在一個函數中使用 yield 語句時,該函數就自動成為一個生成器函數。

yield 的行為類似於 return,但有一個關鍵區別:yield 會「暫停」函數的執行,並將值返回給調用者。當再次向生成器請求值時 (例如在 for 迴圈中,或使用 next() 函數),函數會從上次暫停的地方「恢復」執行,直到遇到下一個 yield 或函數結束。

讓我們看一個簡單的例子:

python
def simple_generator():
    print("Generator started")
    yield 1
    print("Resuming after yielding 1")
    yield 2
    print("Resuming after yielding 2")
    yield 3
    print("Generator finished")

# 建立生成器物件
gen = simple_generator()
print(type(gen)) # <class 'generator'>

# 逐個獲取值
print(f"First value: {next(gen)}")
# Output:
# Generator started
# First value: 1

print(f"Second value: {next(gen)}")
# Output:
# Resuming after yielding 1
# Second value: 2

# 也可以用 for 迴圈迭代
print("Iterating with for loop:")
# gen = simple_generator() # 如果要重新迭代,需要重新建立生成器物件
for value in gen: # 這裡會從 yield 3 開始,因為前面 next(gen) 已經消耗了 1 和 2
    print(value)
# Output:
# Resuming after yielding 2
# 3
# Generator finished

# 如果再次呼叫 next(),會引發 StopIteration
# print(next(gen)) # Raises StopIteration

注意:一旦生成器耗盡 (即所有 yield 都執行完畢或函數返回),它就不能再產生值了。如果嘗試用 next() 從耗盡的生成器中取值,會引發 StopIteration 異常。for 迴圈會自動處理這個異常。

2. 生成器表達式 (Generator Expressions)

生成器表達式提供了一種更簡潔的方式來建立生成器,語法類似於列表推導式 (List Comprehension),但使用圓括號 () 而不是方括號 []

python
# 列表推導式 (List Comprehension) - 立即建立整個列表
list_comp = [x * x for x in range(5)]
print(f"List comprehension: {list_comp}") # Output: [0, 1, 4, 9, 16]
print(type(list_comp)) # <class 'list'>

# 生成器表達式 (Generator Expression) - 建立生成器物件
gen_exp = (x * x for x in range(5))
print(f"Generator expression object: {gen_exp}") # Output: <generator object <genexpr> at 0x...>
print(type(gen_exp)) # <class 'generator'>

print("Values from generator expression:")
for value in gen_exp:
    print(value)
# Output:
# 0
# 1
# 4
# 9
# 16

生成器表達式在語法上更簡潔,非常適合簡單的生成邏輯。

💡 生成器 vs. 一般函數 vs. 列表推導式

特性一般函數 (return)生成器函數 (yield)列表推導式 ([])生成器表達式 (())
返回值單個值或 None生成器物件 (迭代器)列表 (所有元素)生成器物件 (迭代器)
執行方式執行完畢後返回暫停並保存狀態,逐個 yield立即計算並儲存所有元素惰性求值,逐個產生值
記憶體取決於返回值的複雜度非常節省記憶體 (只儲存當前狀態)可能消耗大量記憶體非常節省記憶體 (只儲存當前狀態)
狀態每次調用都是新的開始 (除非有閉包等)記住上次 yield 的位置和局部變數狀態無狀態 (列表本身有狀態)記住上次產生的位置和迭代狀態
用途計算並返回單一結果產生序列數據,特別是大型或無限序列建立和填充列表簡潔地產生序列數據,節省記憶體

一個關鍵點是 yieldreturn 的區別:

  • return: 徹底結束函數的執行,並返回一個值 (或 None)。
  • yield: 暫時中斷函數的執行,返回一個值,並保存函數的當前執行狀態 (包括局部變數)。下次調用 next() 時,函數會從上次離開的地方繼續執行。

🌟 生成器的優點

  1. 記憶體效率 (Memory Efficiency): 這是生成器最顯著的優點。由於它們是惰性求值的,只在需要時才產生值,因此可以處理非常大的數據集,而不會耗盡系統記憶體。

    python
    import sys
    
    # 使用列表推導式
    list_comprehension = [i for i in range(100000)]
    print(f"Memory size of list: {sys.getsizeof(list_comprehension)} bytes")
    
    # 使用生成器表達式
    generator_expression = (i for i in range(100000))
    print(f"Memory size of generator: {sys.getsizeof(generator_expression)} bytes")
    # (注意:生成器物件本身的大小很小,不反映其能產生的數據總量)
  2. 簡化程式碼 (Simplified Code for Iterators): 如果你需要自訂一個迭代器,通常需要實作一個類別,並定義 __iter__()__next__() 方法。生成器函數提供了一種更簡潔、更直觀的方式來達到相同的目的。Python 會自動處理 __iter__()__next__() 的細節。

  3. 處理無限序列 (Handling Infinite Sequences): 生成器可以輕易地表示無限序列,例如自然數序列、斐波那契數列等,因為它們不需要在記憶體中儲存所有元素。

    python
    def infinite_counter(start=0):
        num = start
        while True:
            yield num
            num += 1
    
    counter = infinite_counter()
    # print(next(counter)) # 0
    # print(next(counter)) # 1
    # ...可以無限地獲取下去,直到手動停止

🤔 使用生成器的注意事項

  1. 單次迭代 (Single Pass): 生成器物件只能被完整迭代一次。一旦所有值都被產生完畢 (生成器耗盡),它就不能再用於迭代了。如果你需要再次迭代相同的序列,你需要重新建立一個新的生成器物件。

    python
    gen = (x for x in range(3))
    print(list(gen)) # Output: [0, 1, 2]
    print(list(gen)) # Output: [] (已經耗盡)
  2. 不支援索引和切片 (No Indexing or Slicing): 由於生成器是逐個產生值的,你不能像列表那樣使用索引 (e.g., gen[0]) 或切片 (e.g., gen[1:3]) 來直接存取元素。如果你需要這些功能,最好將生成器的結果轉換為列表 (但要注意記憶體消耗)。

🚀 實際應用場景範例

讀取大型檔案

當處理非常大的文本檔案時,一次將整個檔案讀入記憶體是不明智的。可以使用生成器逐行讀取和處理:

python
def read_large_file(file_path):
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            for line in f:
                yield line.strip() # strip() 移除行尾的換行符
    except FileNotFoundError:
        print(f"Error: File '{file_path}' not found.")
        # yield nothing or raise error further

# 假設有一個名為 'large_log.txt' 的大檔案
# for log_entry in read_large_file('large_log.txt'):
#     if 'ERROR' in log_entry:
#         print(f"Found error: {log_entry}")

這樣,無論檔案有多大,記憶體消耗都保持在很低的水平。

✅ 總結

Python 的生成器是一個非常強大且實用的特性,它透過惰性求值機制,讓我們能夠以極高的記憶體效率處理大型數據集和無限序列。通過使用 yield 關鍵字定義生成器函數,或使用簡潔的生成器表達式,我們可以編寫出更優雅、更易讀、效能更好的 Python 程式碼。

當你下次遇到需要處理大量數據或想創建自訂迭代邏輯時,不妨考慮使用生成器,它可能會給你帶來意想不到的驚喜!

📚 參考資料

KF Software House