Python memory_profiler徹底ガイド
memory_profilerは Python コードの行単位でメモリ使用量を可視化し、リーク箇所を素早く特定できる軽量ツールです。
1. memory_profiler とは?
- 行単位の RSS(常駐メモリ) を計測し、どの行がどれだけメモリを確保・解放したかを一覧表示
- 付属 CLI mprof で、スクリプト全体のメモリ推移をグラフ化
- psutil をバックエンドに使用するシンプル構成で、追加ビルド不要
類似ツールとの比較
| 観点 | memory_profiler | tracemalloc | objgraph | 
|---|---|---|---|
| 測定粒度 | 行単位(RSS) | Python ヒープ、スナップショット単位 | オブジェクト参照グラフ | 
| リーク特定 | ★★★速い | ★★★詳細 | ★★可視化寄り | 
| C拡張メモリ | △ (計測不可) | △ | △ | 
| 導入コスト | pip1行 | 標準 | pip1行 | 
2. インストールと準備
pip install -U memory-profiler psutil   # 必須
pip install matplotlib                  # mprof plot 用(任意)
最小サンプル
from memory_profiler import profile
@profile
def heavy_job():
    a = [i for i in range(10_000_000)]
    b = [i**2 for i in range(10_000_000)]
    del b
    return a
if __name__ == "__main__":
    heavy_job()
実行:
python -m memory_profiler sample.py
結果:
Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
     3     49.7 MiB     49.7 MiB           1   @profile
     4                                         def heavy_job():
     5     52.7 MiB -350195603.5 MiB    10000001       a = [i for i in range(10_000_000)]
     6     34.7 MiB -60282161.9 MiB    10000001       b = [i**2 for i in range(10_000_000)]
     7      6.4 MiB    -28.3 MiB           1       del b
     8      6.4 MiB      0.1 MiB           1       return a
3. ワークフロー概観
- まず mprof run で全体傾向をつかむ
- スパイク地点の関数に @profileを付けて再計測
- 余分な確保行を修正 → 効果確認
4. mprof によるグラフ化
mprof run --interval 0.1 python sample.py   # ログ取得
mprof plot                                   # PNG ウィンドウ表示
mprof peak                                   # 最大値だけ知る
- 
--include-childrenでサブプロセスも追跡
- 
.datは CSV 互換; Excel や pandas で二次解析可能
5. メモリリークデバッグ実践
5.1 再現コード
from memory_profiler import profile
leaks = []
@profile
def leaky(n=1000):
    for _ in range(n):
        leaks.append(bytearray(10**6))  # 1 MB 確保しっぱなし
a = leaky(300)
実行結果サンプル(抜粋):
Line #    Mem usage    Increment  Occurrences   Line Contents
-------------------------------------------------------------
     7     56.0 MiB     56.0 MiB           1   @profile
     8                                         def leaky(n=1000):
     9    294.2 MiB    238.2 MiB         300       leaks.append(bytearray(10**6))
5.2 改善策
- グローバル→ローカルへ変更
- 
del+ ガベコレクション強制
- 
memory_profiler.show_results(timestamp=True)で実行タイミングを確認
修正版:
import gc
from memory_profiler import profile
@profile
def leaky_fixed(n=1000):
    for _ in range(n):
        buf = bytearray(10**6)
        # 使い終わったら参照を切るだけで OK
    gc.collect()
実行結果サンプル(抜粋):
Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    27     49.3 MiB     49.3 MiB           1   @profile
    28                                         def main(n=1000):
    29     53.2 MiB      0.0 MiB         301       for _ in range(n):
    30     53.4 MiB      4.1 MiB         300           buf = bytearray(10**6)
    32     55.2 MiB      0.0 MiB           1       gc.collect()  # RSS がほぼ初期値に戻る
再計測→インクリメントがほぼゼロであることを確認。
5.3 実行結果比較と考察
以下は リークあり と 修正版 をそれぞれ memory_profiler で計測した生ログ抜粋です。
# リークあり
Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    15    275.0 MiB    275.0 MiB           1   @profile
    16                                         def main(n=1000):
    17    379.1 MiB  -1743.0 MiB         301       for _ in range(n):
    18    379.2 MiB  -1608.4 MiB         300           leaks.append(bytearray(10**6))  # 1 MB 確保しっぱなし
# 修正版
Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
    27     49.3 MiB     49.3 MiB           1   @profile
    28                                         def main(n=1000):
    29     53.2 MiB      0.0 MiB         301       for _ in range(n):
    30     53.2 MiB      3.9 MiB         300           buf = bytearray(10**6)
    32     53.2 MiB      0.0 MiB           1       gc.collect()
読み取りポイント
- ピークRSS: リークありは約 379 MiB、修正版は約 53 MiB — 約 7× の差。
- 増加の持続: リーク版ではループ後も 100+ MiB が残留、修正版では元に戻る。
- 
巨大な負の Increment: OS が未使用ページを回収した瞬間にサンプリングが当たると発生。Mem usageが下がっていなければ無視してよい。
- Occurrences × Increment で概算すると、修正版は 1 回につき 13 KB 程度のみ残存 ─ Python オブジェクトのメタデータ分だけで本体は解放されていることがわかる。
結論: グローバルリストへの保持がリークの原因。ローカル変数化 &
gc.collect()で一気に解消できた。
6. Tips & Best Practices
- 
対象を絞る: 高頻度ループ全行に @profileは非推奨
- gc.disable() で GC 影響を排除し、本質的な増加のみを見る
- 環境を揃える: 仮想環境・OS のメモリ管理差異で値が変動
- 
CI 連携: mprof run pytestで回帰テストに最大 RSS を記録
7. まとめ
- memory_profiler は「どこでどれだけ確保したか」を 秒で 把握できる
- mprof → 行単位 → 修正 の三段構えが王道
- C 拡張や巨大 NumPy 配列は別途 tracemalloc・line_profiler と併用しよう
