Pythonで大容量データ処理時のMemoryErrorを修正する方法

intermediate🐍 Python2026-03-21| Python 3.x、Linux / macOS / Windows、RAMに制限のあるマシン

Error Message

MemoryError
#python#メモリ#パフォーマンス#大容量データ

エラーの内容

大きな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万行が妥当な出発点です。そこから調整してください。

Related Error Notes