Python のジェネレータでメモリを節約する
ジェネレータの最大の利点は、メモリ効率の良さです。リストがすべての要素を一度にメモリに展開するのに対し、ジェネレータは必要なときに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回だけ処理する。全データを同時に必要としない。ストリーム処理やパイプライン。
リストが適している
データに何度もアクセスする。インデックスでランダムアクセスしたい。データ量が小さい。
メモリが限られた環境や、巨大なデータを扱う場合は、ジェネレータの活用を検討してください。