エラーの内容
大きなCSVを処理中、データセットをリストに読み込んでいるとき、あるいは重い計算を実行しているとき — Pythonが突然落ちることがあります:
MemoryError
より詳細なトレースバックが表示される場合もあります:
Traceback (most recent call last):
File "process.py", line 12, in <module>
data = [line for line in open('huge_file.csv').readlines()]
MemoryError
Pythonがシステムで確保できないメモリを割り当てようとした結果、RAMが不足したのです。
原因
最もよくある原因は「一度にすべてを読み込む」ことです。たとえば4GBのCSVファイルをreadlines()で読み込むと、Pythonは4GBだけでなく、Pythonオブジェクトのオーバーヘッドによって3〜5倍に膨れ上がるため、データを保持するだけで12〜20GBのRAMが必要になります。
その他の原因:
- ループ内で参照を解放せずに巨大なリストや辞書を構築し続ける
- NumPy演算で大きな中間配列が生成される
- 制限された環境:RAM 2GBのVPS、Dockerコンテナ、CIランナーなど
- 32ビットPythonがプロセスあたり2GBというハードな上限に達する
修正方法1:ファイルをチャンクで読み込む
Pythonのファイルオブジェクトはデフォルトで遅延評価されます。それを活かしましょう。
# 悪い例:ファイル全体をRAMに読み込む
with open('huge_file.txt') as f:
lines = f.readlines() # ここでMemoryError
# 良い例:1行ずつ処理、O(1)のメモリ使用
with open('huge_file.txt') as f:
for line in f:
process(line)
pandasでは、chunksizeパラメータで同様の制御が可能です:
import pandas as pd
for chunk in pd.read_csv('huge_file.csv', chunksize=100_000):
# chunkは10万行のDataFrame — 扱いやすいサイズ
result = chunk.groupby('category')['value'].sum()
save_partial_result(result)
修正方法2:リストの代わりにジェネレータを使う
一度しかループしないのにリストを構築するのは無駄です。ジェネレータは値をその都度計算し、メモリをほとんど消費しません。
# 悪い例:1000万個の整数を一度にRAMに展開
squares = [x**2 for x in range(10_000_000)]
# 良い例:ジェネレータ式、値を1つずつ計算
squares = (x**2 for x in range(10_000_000))
for val in squares:
process(val)
ファイル処理では、yieldを使えば任意の関数をジェネレータにできます:
def read_records(filepath):
with open(filepath) as f:
for line in f:
yield parse(line)
for record in read_records('big.log'):
process(record)
修正方法3:NumPyのdtypeでメモリ使用量を削減する
NumPyのデフォルトはfloat64(1要素あたり8バイト)です。1億要素では800MBになります。float32に切り替えれば400MBに、0〜255の整数にuint8を使えば100MBまで削減できます。
import numpy as np
# デフォルト:float64 = 8バイト/要素 → 1億要素で約800MB
arr = np.array(data)
# float32 = 4バイト/要素 → 約400MB
arr = np.array(data, dtype=np.float32)
# uint8 = 1バイト/要素 → 約100MB
arr = np.array(data, dtype=np.uint8)
pandasでも同じ原則が適用できます。pandasに推測させるのではなく、dtypeを最初から明示しましょう:
df = pd.read_csv('data.csv', dtype={
'user_id': 'int32',
'score': 'float32',
'category': 'category' # 繰り返し文字列 → categoricalで大幅節約
})
修正方法4:メモリマップドファイルを使う
大きなバイナリファイルやNumPy配列には、メモリマッピングを使うとOSにページング制御を委ねられます。ファイル全体を読み込まずに配列のようにアクセスでき、OSは実際にアクセスされたページだけを取得します。
import numpy as np
# ファイル全体を読み込まずにマッピング
arr = np.load('large_array.npy', mmap_mode='r')
# このスライスだけがRAMに読み込まれる
subset = arr[1000:2000]
修正方法5:DaskでOut-of-Coreなデータフレーム処理を行う
RAMに収まらないデータにpandasスタイルの操作が必要な場合、Daskがネイティブに対応しています。APIはほぼ同じですが、実行は遅延評価でチャンク処理されます:
pip install dask[dataframe]
import dask.dataframe as dd
# 遅延計算グラフを構築 — まだ何も読み込まれない
df = dd.read_csv('huge_file.csv')
# これも遅延評価
result = df.groupby('category')['value'].sum()
# .compute()で実際の実行がチャンクごとに行われる
print(result.compute())
修正方法6:オブジェクトを削除してガベージコレクションを強制する
大きなオブジェクトを順番に処理する場合、PythonのGCを待たずに使い終わったオブジェクトを明示的に削除しましょう:
import gc
for batch in batches:
result = process(batch)
save(result)
del result
del batch
gc.collect() # メモリが逼迫している場合は強制回収
これは最後の手段として扱ってください。MemoryErrorが頻発するなら、ジェネレータやチャンク処理への構造変更の方が長期的に効果的です。
修正方法7:スワップ領域を追加する(Linux)
すぐにリファクタリングできないサーバーでは、スワップ領域を追加することでクラッシュを防げます(速度は犠牲になりますが)。SSD上の4GBスワップファイルはRAMと比べて10〜20倍遅くなることがありますが、プロセスがクラッシュするよりはマシです。
# 現在の状態を確認
free -h
# 4GBのスワップファイルを作成
sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
これは時間稼ぎに過ぎません。根本的な原因の解決にはなりません。
修正の確認
memory_profilerを使って、変更によってピーク使用量が実際に減ったか確認しましょう:
pip install memory-profiler
from memory_profiler import profile
@profile
def my_function():
# ここにコードを書く
pass
my_function()
出力では行ごとのメモリ使用量が表示されます。チャンク処理やジェネレータに切り替えた後、ほとんどのワークロードではピークRAMがギガバイト単位から数十メガバイト単位まで下がるはずです。
スクリプト実行中に別のターミナルでリアルタイムの使用量を監視するには:
watch -n 1 'free -h'
予防策
- **スケールアップ前にプロファイリング:**まず小さなサンプルで
memory_profilerを実行しましょう。1000行で10倍のメモリスパイクを発見する方が、1000万行で発見するよりはるかにコストが低いです。 - **デフォルトでジェネレータを使う:**シーケンスを生成する関数は、リスト全体を実体化する具体的な理由がない限り、
yieldを使うべきです。 - **pandasで明示的なdtypeを設定する:**pandasに型推論を任せると、すべてが
int64/float64になります。50カラムのデータセットでは、必要なメモリの2〜4倍を消費することが多いです。 - **チャンクサイズをベンチマークする:**小さすぎるとI/Oオーバーヘッドが増え、大きすぎるとメモリが急増します。多くのワークロードでは、1チャンクあたり5万〜20万行が妥当な出発点です。そこから調整してください。

