Pythonista必見!memory_profilerでメモリ使用量を徹底解剖

Pythonは書きやすく強力な言語ですが、特に大規模なデータ処理や長時間稼働するアプリケーションでは、メモリ使用量が問題になることがあります。「なんかプログラムが遅いな…」「いつの間にかメモリを使いすぎている…?」と感じたことはありませんか? そんな時に役立つのが、Pythonのメモリプロファイリングライブラリ memory_profiler です。 このブログ記事では、memory_profiler の基本的な使い方から応用的な機能まで、具体的なコード例を交えながら詳しく解説します。メモリの挙動を理解し、より効率的なPythonコードを書くための第一歩を踏み出しましょう!

memory_profiler とは?

memory_profiler は、Pythonプログラムのメモリ消費量を監視・分析するためのライブラリです。主に以下の機能を提供します。

  • 行ごとのメモリ使用量分析: 関数内の各行が実行された後のメモリ使用量と、その行でのメモリ増加量を計測します。これにより、どのコード行がメモリを多く消費しているかを特定できます。
  • 時系列でのメモリ使用量監視: プログラム実行中のメモリ使用量の推移を記録し、グラフとして可視化できます。メモリリークの発見などに役立ちます。
  • シンプルな導入: @profile デコレータを関数に追加するだけで、基本的なプロファイリングを開始できます。
  • Jupyter/IPython連携: マジックコマンドを使って、Jupyter NotebookやIPython環境で手軽にメモリ使用量を測定できます。

このライブラリは純粋なPythonで書かれており、内部で psutil ライブラリを利用してプロセスのメモリ情報を取得しています。

注意: PyPIページによると、memory_profiler は現在活発にはメンテナンスされていないようです(”This package is no longer actively maintained.” との記載あり)。しかし、依然として多くのプロジェクトで利用されており、基本的なメモリプロファイリングには十分役立ちます。代替ツールとしては MemrayFil なども存在します。

インストール方法

memory_profiler のインストールは pip を使って簡単に行えます。依存ライブラリである psutil も一緒にインストールされます。

pip install -U memory_profiler

また、時系列グラフの描画機能(後述する mprof plot)を使用する場合は、matplotlib も必要になります。必要に応じてインストールしてください。

pip install matplotlib

conda (conda-forge) を使用している場合もインストール可能です。

conda install -c conda-forge memory_profiler

基本的な使い方: 行ごとのメモリプロファイリング

最も基本的な使い方は、メモリ使用量を計測したい関数に @profile デコレータを追加することです。line_profiler ライブラリを使ったことがある方は、同様の感覚で利用できます。

例として、大きなリストを作成・削除する簡単な関数をプロファイリングしてみましょう。

sample_script.py:

from memory_profiler import profile

@profile
def create_large_list():
    print("関数開始")
    a = [1] * (10 ** 6)  # 100万要素のリストを作成
    print("リストa作成完了")
    b = [2] * (2 * 10 ** 7) # 2000万要素のリストを作成
    print("リストb作成完了")
    del b               # リストbを削除
    print("リストb削除完了")
    return a

if __name__ == '__main__':
    print("スクリプト開始")
    result = create_large_list()
    print("スクリプト終了")

このスクリプトを実行するには、通常の python sample_script.py ではなく、-m memory_profiler オプションを付けて Python インタープリタを起動します。

python -m memory_profiler sample_script.py

実行すると、標準出力に以下のようなプロファイリング結果が表示されます (値は環境によって異なります)。

スクリプト開始
関数開始
リストa作成完了
リストb作成完了
リストb削除完了
スクリプト終了
Filename: sample_script.py

Line #    Mem usage    Increment  Occurrences   Line Contents
=============================================================
     3   53.0 MiB     53.0 MiB           1   @profile
     4                                         def create_large_list():
     5   53.0 MiB      0.0 MiB           1       print("関数開始")
     6   60.6 MiB      7.6 MiB           1       a = [1] * (10 ** 6)  # 100万要素のリストを作成
     7   60.6 MiB      0.0 MiB           1       print("リストa作成完了")
     8  213.1 MiB    152.5 MiB           1       b = [2] * (2 * 10 ** 7) # 2000万要素のリストを作成
     9  213.1 MiB      0.0 MiB           1       print("リストb作成完了")
    10   60.6 MiB   -152.5 MiB           1       del b               # リストbを削除
    11   60.6 MiB      0.0 MiB           1       print("リストb削除完了")
    12   60.6 MiB      0.0 MiB           1       return a

出力結果の見方

列名 説明
Line # プロファイリング対象のコード行番号
Mem usage その行の実行のPythonインタプリタの総メモリ使用量 (MiB: Mebibyte)
Increment 前の行からのメモリ使用量の増減
Occurrences その行が実行された回数 (ループなどで複数回実行される場合に増加)
Line Contents プロファイリング対象のコード行の内容

この結果から、8行目の b = [2] * (2 * 10 ** 7) でメモリ使用量が約 152.5 MiB 増加し、10行目の del b でそのメモリが解放されていることがわかります。このように、どの行でメモリが多く確保・解放されているかを具体的に把握できます。

Tips: @profile デコレータは、計測したい関数にのみ付与します。複数の関数に付与することも可能です。
Tips: 結果をファイルに出力したい場合は、@profile(stream=f) のように、ファイルオブジェクトを stream 引数に渡すことができます。
import io
from memory_profiler import profile

output_log = io.StringIO() # または open('memory_log.txt', 'w+')

@profile(stream=output_log)
def some_function():
    # ... 処理 ...
    pass

if __name__ == '__main__':
    some_function()
    print(output_log.getvalue()) # 内容を確認
    # output_log.close() # ファイルの場合は閉じる

応用的な使い方

1. 時系列でのメモリ使用量監視 (`mprof`)

memory_profiler には mprof というコマンドラインツールが付属しており、プログラム実行中のメモリ使用量の変化を記録し、グラフとしてプロットすることができます。これは、メモリリークの調査や、時間経過に伴うメモリパターンの把握に非常に便利です。

まず、mprof run コマンドで対象のスクリプトを実行します。この際、スクリプト内の @profile デコレータは不要です(むしろ、from memory_profiler import profile のインポートがあると、mprof run がうまく動作しない場合があります)。

mprof run sample_script.py

実行すると、カレントディレクトリに mprofile_<timestamp>.dat という形式のデータファイルが生成されます。このファイルに時系列のメモリ使用量データが記録されています。

次に、mprof plot コマンドで記録されたデータをグラフ化します。デフォルトでは最後に生成された .dat ファイルが使用されます。

mprof plot

これにより、matplotlib を利用したメモリ使用量の推移グラフが表示されます(matplotlibがインストールされている場合)。特定の .dat ファイルを指定してプロットすることも可能です。

mprof plot mprofile_20250405123456.dat -o memory_usage_graph.png

-o オプションでグラフを画像ファイルとして保存できます。

mprof コマンドには他にもサブコマンドがあります:

  • mprof list: 記録された .dat ファイルの一覧を表示します。
  • mprof clean: 記録されたすべての .dat ファイルを削除します。
  • mprof rm <file>: 特定の .dat ファイルを削除します。

2. Jupyter / IPython での使用 (%memit, %%memit)

Jupyter Notebook や IPython 環境では、さらに手軽にメモリプロファイリングを行えるマジックコマンドが提供されています。

まず、拡張機能をロードします。

%load_ext memory_profiler

%memit (ラインマジック): 特定の Python 文の実行に伴うメモリ使用量の変化を測定します。

import numpy as np

# %memit を使って numpy 配列生成のメモリ増加量を計測
%memit large_array = np.zeros((1000, 1000))

出力例:

peak memory: 150.24 MiB, increment: 7.63 MiB

peak memory はその行を実行した後のピークメモリ使用量、increment はその行の実行によるメモリ増加量を示します。

%%memit (セルマジック): セル全体のコード実行に伴うメモリ使用量の変化を測定します。

%%memit

def process_data(size):
    data = list(range(size))
    processed = [x * 2 for x in data]
    return processed

result = process_data(10**6)

出力例:

peak memory: 185.50 MiB, increment: 43.18 MiB

これらのマジックコマンドを使うと、コードを直接変更することなく、インタラクティブにメモリ使用量をチェックできるため、データ分析や実験的なコード記述の際に非常に便利です。ただし、Jupyter Notebook のキャッシュの挙動により、del で変数を削除しても期待通りにメモリが解放されないように見える場合がある点には注意が必要です。

3. 特定関数のプロファイリング (デコレータなし)

ソースコードを変更せずに特定の関数だけをプロファイリングしたい場合もあります。memory_profiler モジュールはプログラム的に使用することも可能です。

from memory_profiler import memory_usage

def function_to_profile():
    a = [1] * (10**6)
    b = [2] * (5 * 10**6)
    del b
    return a

def another_function():
    c = "some string" * 1000
    return c

if __name__ == '__main__':
    # function_to_profile の実行中のメモリ使用量を 0.1 秒間隔で取得
    # timeout は計測時間の上限(秒)、None で制限なし
    # retval=True で関数の戻り値も取得
    mem_usage, return_value = memory_usage(
        (function_to_profile, (), {}), # (関数, argsタプル, kwargs辞書)
        interval=0.1,
        timeout=None,
        retval=True,
        max_usage=True # 戻り値として最大メモリ使用量も返す
    )

    print(f"function_to_profile の最大メモリ使用量: {max(mem_usage) if mem_usage else 'N/A'} MiB")
    print(f"戻り値の最初の10要素: {return_value[:10]}")

    # 別の関数の実行 (これはプロファイリングされない)
    other_result = another_function()
    print("another_function 実行完了")

memory_usage 関数を使うことで、指定した関数 (やプロセス) の実行中のメモリ使用量をリストとして取得できます。interval でサンプリング間隔を、timeout で計測時間を制御できます。max_usage=True を指定すると、記録されたメモリ使用量のリストの代わりに、単一の最大メモリ使用量の値を返します。

実践的なユースケースとヒント

memory_profiler は以下のような場面で特に役立ちます。

  • メモリリークの特定: 長時間実行するアプリケーションで mprof を使い、メモリ使用量が時間とともに増加し続ける箇所を探します。
  • データ処理パイプラインの最適化: 大規模なデータセットを扱う際、各処理ステップでのメモリ消費を @profile%%memit で確認し、ボトルネックとなっている箇所(例えば、巨大な中間データをメモリ上に保持している箇所)を特定して改善します(例:ジェネレータの使用、チャンク処理の導入)。
  • アルゴリズム比較: 同じ目的を達成する複数のアルゴリズムについて、実行時間だけでなくメモリ効率も比較検討します。
  • ライブラリ/関数のメモリ特性理解: 使用しているライブラリの特定の関数が内部でどれだけのメモリを消費するかを確認します。

効果的に使うためのヒント

  • 代表的なワークロードで計測する: 実際の利用状況に近いデータや処理フローでプロファイリングを行うことが重要です。
  • プロファイリングのオーバーヘッドを考慮する: memory_profiler 自体も多少のメモリとCPU時間を消費します。特に interval を短く設定するとオーバーヘッドが増加する可能性があります。
  • 他のツールと組み合わせる: CPU時間のボトルネックを特定する cProfileline_profiler と組み合わせることで、パフォーマンス全体を俯瞰的に分析できます。まず cProfile で遅い関数を特定し、次にその関数に対して memory_profilerline_profiler を適用するといった使い方が効果的です。
  • 結果を解釈する: メモリ使用量の増加が必ずしも問題とは限りません。必要なデータを保持するためのメモリ確保は当然発生します。問題となるのは、不要になったメモリが解放されないリークや、非効率なデータ構造による過剰なメモリ消費です。

まとめ

memory_profiler は、Python アプリケーションのメモリ使用量を理解し、最適化するための強力なツールです。行ごとの詳細な分析から時系列での監視まで、様々な角度からメモリの挙動を探ることができます。 インストールも使い方も比較的簡単なので、メモリ関連の問題に直面したときや、より効率的なコードを目指す際には、ぜひこのライブラリを活用してみてください。メモリ使用量を意識した開発は、アプリケーションの安定性とパフォーマンス向上に繋がります!

公式ドキュメントやリソースは PyPI の memory-profiler ページ を参照してください。