ジェネレータの最大の利点は、メモリ効率の良さです。リストがすべての要素を一度にメモリに展開するのに対し、ジェネレータは必要なときに1つずつ値を生成します。この「遅延評価」により、大量のデータを扱う際にメモリを大幅に節約できます。
メモリ使用量の比較
100万個の数値を扱う場合の違いを見てみましょう。
import sys # リスト:すべてメモリに展開 numbers_list = [x ** 2 for x in range(1_000_000)] print(f"リスト: {sys.getsizeof(numbers_list):,} bytes") # リスト: 8,448,728 bytes(約8MB) # ジェネレータ:計算式だけを保持 numbers_gen = (x ** 2 for x in range(1_000_000)) print(f"ジェネレータ: {sys.getsizeof(numbers_gen)} bytes") # ジェネレータ: 200 bytes
ジェネレータはデータそのものではなく「どうやってデータを生成するか」だけを覚えているため、要素数に関わらずサイズがほぼ一定です。
大きなファイルの処理
巨大なファイルを処理する際、全行をリストに読み込むとメモリ不足になることがあります。
リストで読み込む(危険)
メモリに全行を展開する。数GBのファイルで OutOfMemoryError の可能性。
ジェネレータで処理(安全)
1行ずつ読み込んで処理。ファイルサイズに関係なく一定のメモリで動作。
# 悪い例:全行をリストに読み込む def read_all_lines(filename): with open(filename) as f: return f.readlines() # 全行がメモリに # 良い例:ジェネレータで1行ずつ def read_lines(filename): with open(filename) as f: for line in f: yield line.strip() # 使用例 for line in read_lines("huge_file.txt"): process(line) # 1行ずつ処理
データパイプラインの構築
ジェネレータを連結すると、メモリ効率の良いデータパイプラインを作れます。
def read_numbers(filename): with open(filename) as f: for line in f: yield int(line.strip()) def filter_positive(numbers): for n in numbers: if n > 0: yield n def double(numbers): for n in numbers: yield n * 2 # パイプラインを構築(まだ何も実行されない) pipeline = double(filter_positive(read_numbers("data.txt"))) # ここで初めて1行ずつ処理が実行される for result in pipeline: print(result)
各ジェネレータは1要素ずつ処理するため、ファイル全体がメモリに載ることはありません。
sum() や max() との組み合わせ
集計処理でもジェネレータを活用できます。
# リスト内包表記(一度リストを作成) total = sum([x ** 2 for x in range(1_000_000)]) # ジェネレータ式(リストを作らない) total = sum(x ** 2 for x in range(1_000_000))
後者はリストを作らずに直接合計を計算するため、メモリ使用量が大幅に少なくなります。
いつジェネレータを使うべきか
ジェネレータが適している
大量のデータを1回だけ処理する。全データを同時に必要としない。ストリーム処理やパイプライン。
リストが適している
データに何度もアクセスする。インデックスでランダムアクセスしたい。データ量が小さい。
メモリが限られた環境や、巨大なデータを扱う場合は、ジェネレータの活用を検討してください。