[python] matplotlib.pyplotの挙動がわかり始めたメモ


グラフ描画用のモジュールであるpyplotは時々お世話になるのですが、なかなか使い方に慣れずにいました。最近ようやくそのもやもやを解消できたので、気づきのメモとして残しておきます。
※ 「なぜそのような設計になっているのか」ではなく「どのような設計になっているのか」という内容の話になります。

個人的によくわからなかったところ

まずは基本的なグラフ出力です。

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-np.pi, np.pi)
y = np.sin(x)

fig = plt.figure()
plt.plot(x, y)
plt.title("y = sin(x)")
plt.xlabel("x")
plt.ylabel("y")
fig.savefig("output.png")

これで画像が output.png として出力されるのですが、個人的にこの時の挙動がどうも腑に落ちませんでした。たとえば

  • plt インスタンスを生成した形跡がない
  • グラフのタイトルなどを変更するのは plt のメソッドを使っていたのに、画像として保存する場合は fig のメソッドを呼び出している
  • fig のように fig1 , fig2 , ...を生成しただけで plt でアクセスするときの対象も fig1 , fig2 , ...と順に変化する

といったあたりです(ちなみに title()xlabel() などがクラスメソッドであるという説は考えていませんでした)。

個人的にはインスタンス生成→メソッド呼び出しの流れに慣れているので、下のようなやり方であったとすれば抵抗なく理解していたと思います。

# これは正常に動作しません。
import matplotlib
import numpy as np

x = np.linspace(-np.pi, np.pi)
y = np.sin(x)

plt = matplotlib.pyplot() # ここでインスタンスを生成してほしい
plt.plot(x, y)
plt.title("y = sin(x)")
plt.xlabel("x")
plt.ylabel("y")
plt.figure.savefig("output.png") # ここはpltインスタンスのメソッドであってほしい

なぜ最初のやり方で正常に実行できるのでしょうか?
そしてなぜ私の想定したやり方ではうまく動作しないのでしょうか?

実際どうなっているのか

最初の正常に動作するスクリプトに登場した要素がそれぞれ何者なのかを確認します(numpyの部分は省略します)。

  • matplotlib
    • グラフ描画用のライブラリ
  • pyplot
    • matplotlib ライブラリの中のモジュール
    • import matplotlib.pyplot 等でimportして使用
  • plt
    • matplotlib.pyplot のエイリアス(従ってtypeはmodule)
    • エイリアスなので好きな名前に変更可
  • fig
    • matplotlib.figure.Figure クラスのインスタンス
    • fig = pyplot.figure() で生成される

重要なのは plt がクラスではなくモジュール ということです。 plt がクラスであったとすればどこかでインスタンスを生成して欲しいところですが、モジュールゆえに2行目でimportした時既に生成されていたため、突然 fig = plt.figure()plt.plot(x, y) としても問題なく動作していたのですね。
また、これは推測ですが、 fig インスタンスが生成される際に plt の中でアクセス先が更新されると考えれば他の疑問点も納得できそうです。

おまけ

さて、pythonにおいてモジュールはシングルトンとして生成されるので、どこで呼び出しても同じidのオブジェクトを参照します。従って、 pyplot を別の場所で呼び出した際も、既に作成済みのグラフにアクセスすることができます。

たとえば、次の2つのファイルが同一ディレクトリにあったとします。

graph.py
import matplotlib.pyplot as plt
import numpy as np

def edit_graph():
    x = np.linspace(-np.pi, np.pi)
    y = np.sin(x)

    plt.plot(x, y)
    plt.title("y = sin(x)")
    plt.xlabel("x")
    plt.ylabel("y")
main.py
import matplotlib.pyplot as plt_main
import graph

graph.edit_graph()
plt_main.show()

この状態で python main.py を実行すると、先程と同様の正弦波のグラフが出力されます。つまり、あるスクリプトファイル内で生成・編集したグラフを別のスクリプトファイルで出力するということが、インスタンスの生成・受け渡し無しでできるということです(設計上あまり推奨されない気はしますが)。
matplotlib.figure.Figure クラス内のメソッドである savefig() を呼び出す場合も、 fig = pyplot.figure() を実行した後であれば可能です。

まとめ

今回の疑問は、私自身が plt を何かしらのクラスorインスタンスだと勘違いしていたことが主な原因でした。なぜそう思っていたのか、今となってはわかりません... plt がモジュールであるということに気付けば大した問題ではありませんが、改めて意識してみるとより理解が深まると思います。

最後に今回気付いたことのまとめです。

  • pyplot はモジュールであり、普段は plt のような名前をつけて利用している
  • pyplot モジュール自体がクラスあるいはそのインスタンスのようにふるまう
  • fig = pyplot.figure() を実行するたびに matplotlib.figure.Figure クラスの fig インスタンスが生成され、そのたびに pyplot モジュールによるアクセスの対象が移る