インタラクティブな図と非同期プログラミング#

Matplotlib は、Figure を GUI ウィンドウに埋め込むことで、豊富なインタラクティブな Figure をサポートします。Axes でデータを検査するためのパンとズームの基本的な相互作用は、Matplotlib に「焼き付け」られています。これは、マウスとキーボードの完全なイベント処理システムによってサポートされており、高度なインタラクティブ グラフの作成に使用できます。

このガイドは、Matplotlib と GUI イベント ループの統合がどのように機能するかについて、低レベルの詳細を紹介することを目的としています。Matplotlib イベント API のより実用的な紹介については、イベント処理システム対話型チュートリアル、および Matplotlib を使用した対話型アプリケーションを参照してください。

イベントループ#

基本的に、すべてのユーザー インタラクション (およびネットワーク) は、ユーザーからのイベント (OS 経由) を待機し、それに対して何らかの処理を行う無限ループとして実装されます。たとえば、最小限の Read Evaluate Print Loop (REPL) は

exec_count = 0
while True:
    inp = input(f"[{exec_count}] > ")        # Read
    ret = eval(inp)                          # Evaluate
    print(ret)                               # Print
    exec_count += 1                          # Loop

これには多くの細かい点が欠けています (たとえば、最初の例外で終了します!) が、すべての端末、GUI、およびサーバーの根底にあるイベント ループを表しています[ 1 ]。一般に、読み取りステップは、ユーザー入力であれネットワークであれ、ある種の I/O を待機していますが、評価印刷は入力を解釈し、それに対して何らかの処理を行います。

実際には、I/O ループを直接実装するのではなく、特定のイベントに応答して実行されるコールバックを登録するメカニズムを提供するフレームワークと対話します[ 2 ]。たとえば、「ユーザーがこのボタンをクリックしたときに、この関数を実行してください」または「ユーザーが 'z' キーを押したときに、この別の関数を実行してください」などです。これにより、ユーザーは、I/Oの核心[ 3 ]の詳細を掘り下げなくても、リアクティブでイベント駆動型のプログラムを作成できます。コア イベント ループは「メイン ループ」と呼ばれることもあり、ライブラリによっては、_execrun、 などの名前のメソッドによって通常開始されますstart

すべての GUI フレームワーク (Qt、Wx、Gtk、tk、OSX、または Web) には、ユーザー インタラクションをキャプチャしてアプリケーションに戻す何らかの方法があります ( Qt のSignal/Slotフレームワークなど) が、正確な詳細はツールキットによって異なります。Matplotlib には、ツールキット API を使用してツールキット UI イベントを Matplotlib のイベント処理システムにブリッジする、サポートする各 GUI ツールキットのバックエンドがあります。その後、関数を Matplotlib のイベント処理システムに接続するために使用でき ます。これにより、データと直接対話し、GUI ツールキットに依存しないユーザー インターフェイスを作成できます。FigureCanvasBase.mpl_connect

コマンドプロンプトの統合#

ここまでは順調ですね。コードをインタラクティブにインタープリターに送信して結果を返すことができる REPL (IPython ターミナルのような) があります。また、ユーザー入力を待機するイベント ループを実行し、それが発生したときに実行する関数を登録できる GUI ツールキットもあります。ただし、両方を実行したい場合は問題があります。プロンプトと GUI イベント ループはどちらも無限ループであり、それぞれ が担当していると考えられます。プロンプトと GUI ウィンドウの両方が応答するようにするには、ループを「タイムシェア」できるようにする方法が必要です。

  1. インタラクティブなウィンドウが必要な場合は、GUI メイン ループで python プロセスをブロックします。

  2. CLI のメイン ループが Python プロセスをブロックし、断続的に GUI ループを実行するようにします。

  3. Python を GUI に完全に埋め込む (ただし、これは基本的に完全なアプリケーションを作成することです)

プロンプトをブロックする#

pyplot.show

開いているすべての Figure を表示します。

pyplot.pause

interval秒間、GUI イベント ループを実行します。

backend_bases.FigureCanvasBase.start_event_loop

ブロッキング イベント ループを開始します。

backend_bases.FigureCanvasBase.stop_event_loop

現在のブロッキング イベント ループを停止します。

最も単純な「統合」は、「ブロッキング」モードで GUI イベント ループを開始し、CLI を引き継ぐことです。GUI イベント ループの実行中は、プロンプトに新しいコマンドを入力することはできません (端末は、入力された文字を端末にエコーする場合がありますが、GUI イベント ループの実行でビジーであるため、Python インタープリターには送信されません)。 Figure ウィンドウは応答します。イベント ループが停止すると (まだ開いている Figure ウィンドウが応答しなくなると)、プロンプトを再び使用できるようになります。イベント ループを再開すると、開いているすべての Figure が再び応答するようになります (キューに入れられたユーザー インタラクションが処理されます)。

開いているすべての Figure が閉じられるまでイベント ループを開始するには、次のように使用 pyplot.showします。

pyplot.show(block=True)

一定時間 (秒単位) だけイベント ループを開始するには、 を使用します pyplot.pause

を使用していない場合は、および pyplotを介してイベント ループを開始および停止できます。ただし、使用しないほとんどのコンテキストでは、Matplotlib を大規模な GUI アプリケーションに埋め込んでおり、そのアプリケーションに対して GUI イベント ループが既に実行されているはずです。FigureCanvasBase.start_event_loopFigureCanvasBase.stop_event_looppyplot

プロンプトから離れて、この手法は、ユーザーの操作のために一時停止するスクリプトを作成したり、追加データのポーリングの間に Figure を表示したりする場合に非常に役立ちます。詳細については、スクリプトと関数 を参照してください。

入力フックの統合#

ブロッキング モードで GUI イベント ループを実行したり、UI イベントを明示的に処理したりすることは便利ですが、もっとうまくやることもできます。使用可能なプロンプトと対話型の Figure ウィンドウを使用できるようにしたいと考えています。

これは、インタラクティブ プロンプトの「入力フック」機能を使用して行うことができます。このフックは、ユーザーが入力するのを待つときにプロンプ​​トによって呼び出されます (タイピングが速い人でも、プロンプトはほとんどの場合、人間が考えて指を動かすのを待っています)。詳細はプロンプトによって異なりますが、ロジックは大まかに

  1. キーボード入力の待機を開始します

  2. GUI イベント ループを開始する

  3. ユーザーがキーを押すとすぐに、GUI イベント ループを終了し、キーを処理します。

  4. 繰り返す

これにより、インタラクティブな GUI ウィンドウとインタラクティブなプロンプトが同時に表示されているように見えます。ほとんどの場合、GUI イベント ループが実行されていますが、ユーザーが入力を開始するとすぐにプロンプ​​トが再び表示されます。

このタイムシェア手法では、Python がアイドル状態でユーザー入力を待機している間だけ、イベント ループを実行できます。長時間コードを実行しているときに GUI を応答させたい場合は、上記のように GUI イベント キューを定期的にフラッシュする必要があります。この場合、プロセスをブロックしているのはREPLではなくコードであるため、「タイムシェア」を手動で処理する必要があります。逆に、図形の描画が非常に遅いと、描画が完了するまでプロンプトがブロックされます。

完全埋め込み#

別の方向に進んで、Figure (およびPython インタープリター) をリッチ ネイティブ アプリケーションに完全に埋め込むことも可能です。Matplotlib は、GUI アプリケーションに直接埋め込むことができる各ツールキットのクラスを提供します (これが組み込みウィンドウの実装方法です!)。詳細については、グラフィカル ユーザー インターフェイスへの Matplotlib の埋め込みを参照してください。

スクリプトと関数#

backend_bases.FigureCanvasBase.flush_events

Figure の GUI イベントをフラッシュします。

backend_bases.FigureCanvasBase.draw_idle

制御が GUI イベント ループに戻ったら、ウィジェットの再描画を要求します。

figure.Figure.ginput

Figure を操作するための呼び出しをブロックしています。

pyplot.ginput

Figure を操作するための呼び出しをブロックしています。

pyplot.show

開いているすべての Figure を表示します。

pyplot.pause

interval秒間、GUI イベント ループを実行します。

スクリプトでインタラクティブな Figure を使用するには、いくつかの使用例があります。

  • ユーザー入力をキャプチャしてスクリプトを操作する

  • 長時間実行されるスクリプトの進行に合わせて進行状況が更新されます

  • データ ソースからの更新のストリーミング

ブロッキング関数#

Axes でポイントを収集する必要がある場合のみ使用できます。 Figure.ginputより一般的には、ツール blocking_inputのツールがイベント ループの開始と停止を処理します。ただし、カスタム イベント処理を作成した場合や使用している場合は、上記widgetsの方法を使用して GUI イベント ループを手動で実行する必要があります。

プロンプトのブロックで説明されている方法を使用して 、GUI イベント ループの実行を中断することもできます。ループが終了すると、コードが再開されます。一般に、インタラクティブなフィギュアの追加の利点を使用して、代わりに使用する場所を使用time.sleepできます 。pyplot.pause

たとえば、データをポーリングする場合は、次のようなものを使用できます

fig, ax = plt.subplots()
ln, = ax.plot([], [])

while True:
    x, y = get_new_data()
    ln.set_data(x, y)
    plt.pause(1)

これにより、新しいデータがポーリングされ、図が 1Hz で更新されます。

イベントループを明示的にスピン#

backend_bases.FigureCanvasBase.flush_events

Figure の GUI イベントをフラッシュします。

backend_bases.FigureCanvasBase.draw_idle

制御が GUI イベント ループに戻ったら、ウィジェットの再描画を要求します。

保留中の UI イベント (マウスのクリック、ボタンの押下、または描画) がある開いているウィンドウがある場合は、 を呼び出すことによって、これらのイベントを明示的に処理できますFigureCanvasBase.flush_events。これにより、現在待機中のすべての UI イベントが処理されるまで、GUI イベント ループが実行されます。正確な動作はバックエンドに依存しますが、通常はすべての Figure のイベントが処理され、処理待ちのイベント (処理中に追加されたものではない) のみが処理されます。

例えば

import time
import matplotlib.pyplot as plt
import numpy as np
plt.ion()

fig, ax = plt.subplots()
th = np.linspace(0, 2*np.pi, 512)
ax.set_ylim(-1.5, 1.5)

ln, = ax.plot(th, np.sin(th))

def slow_loop(N, ln):
    for j in range(N):
        time.sleep(.1)  # to simulate some work
        ln.figure.canvas.flush_events()

slow_loop(100, ln)

これは少し遅れているように感じますが (ユーザー入力を 100 ミリ秒ごとに処理しているだけですが、20 ~ 30 ミリ秒が「応答性が高い」と感じられるため)、応答します。

プロットに変更を加えて再レンダリングしたい場合はdraw_idle、キャンバスの再描画を要求するために呼び出す必要があります。このメソッドは、draw_soonと同様に 考えることができますasyncio.loop.call_soon

これを上記の例に次のように追加できます。

def slow_loop(N, ln):
    for j in range(N):
        time.sleep(.1)  # to simulate some work
        if j % 10:
            ln.set_ydata(np.sin(((j // 10) % 5 * th)))
            ln.figure.canvas.draw_idle()

        ln.figure.canvas.flush_events()

slow_loop(100, ln)

より頻繁に呼び出すFigureCanvasBase.flush_eventsほど、Figure の応答性が向上しますが、視覚化により多くのリソースを消費し、計算にかかる費用が少なくなります。

古いアーティスト#

アーティスト (Matplotlib 1.5 以降) には、最後にレンダリングされてからアーティストの内部状態が変更された場合のstale属性があり ます。Trueデフォルトでは、stale 状態は描画ツリー内のアーティストの親まで伝播されます。たとえば、Line2D インスタンスの色が変更された場合、AxesそれFigureを含む と も「stale」としてマークされます。したがって、fig.staleフィギュア内のアーティストが変更されていて、画面に表示されているものと同期していないかどうかが報告されます。draw_idleこれは、Figure の再レンダリングをスケジュールするために を呼び出す必要があるかどうかを判断するために使用することを目的としています。

各アーティストにはArtist.stale_callback、署名付きのコールバックを保持する属性があります

def callback(self: Artist, val: bool) -> None:
   ...

デフォルトでは、古い状態をアーティストの親に転送する関数に設定されています。特定のアーティストの伝播を抑制したい場合は、この属性を None に設定します。

Figureインスタンスにはアーティストが含まれておらず、デフォルトのコールバックはNoneです。呼び出したときに不在の場合は、pyplot.ionが 古くなったとき IPythonに呼び出すコールバックをインストールします 。では、フックを使用して 、ユーザーの入力を実行した後、ユーザーにプロンプ​​トを返す前に、古い Figureを呼び出し ます。使用していない場合は、コールバック 属性を使用して、Figure が古くなったときに通知を受けることができます。draw_idleFigureIPython'post_execute'draw_idlepyplotFigure.stale_callback

アイドルドロー#

backend_bases.FigureCanvasBase.draw

をレンダリングしFigureます。

backend_bases.FigureCanvasBase.draw_idle

制御が GUI イベント ループに戻ったら、ウィジェットの再描画を要求します。

backend_bases.FigureCanvasBase.flush_events

Figure の GUI イベントをフラッシュします。

ほとんどの場合、 backend_bases.FigureCanvasBase.draw_idleover を使用することをお勧めしbackend_bases.FigureCanvasBase.drawます。drawFigure のレンダリングを強制するのに対しdraw_idle、次に GUI ウィンドウが画面を再描画するときにレンダリングをスケジュールします。これにより、画面に表示されるピクセルのみがレンダリングされるため、パフォーマンスが向上します。画面ができるだけ早く更新されていることを確認したい場合は、

fig.canvas.draw_idle()
fig.canvas.flush_events()

ねじ切り#

ほとんどの GUI フレームワークでは、画面に対するすべての更新、つまりメイン イベント ループをメイン スレッドで実行する必要があります。これにより、プロットの定期的な更新をバックグラウンド スレッドにプッシュすることができなくなります。逆のように見えますが、通常は、計算をバックグラウンド スレッドにプッシュし、メイン スレッドで Figure を定期的に更新する方が簡単です。

一般に、Matplotlib はスレッドセーフではありません。あるスレッドでオブジェクトを更新 Artistし、別のスレッドから描画する場合は、クリティカル セクションでロックしていることを確認する必要があります。

イベントループ統合メカニズム#

CPython/readline #

Python C API は、PyOS_InputHook実行する関数を登録するためのフック を提供します (「関数は、Python のインタープリター プロンプトがアイドル状態になり、端末からのユーザー入力を待つときに呼び出されます。」)。このフックは、2 番目のイベント ループ (GUI イベント ループ) を Python の入力プロンプト ループと統合するために使用できます。フック関数は、通常、GUI イベント キューで保留中のすべてのイベントを使い果たし、メイン ループを短い一定時間実行するか、stdin でキーが押されるまでイベント ループを実行します。

PyOS_InputHookMatplotlib はさまざまな方法で使用されるため、現在、Matplotlib は管理を行っていません。この管理は、ダウンストリーム ライブラリ (ユーザー コードまたはシェル) に任されています。「インタラクティブ モード」の Matplotlib を使用した場合でも、適切な図PyOS_InputHookが登録されていない場合、バニラの Python repl ではインタラクティブな図が機能しない場合があります。

入力フックとそれらをインストールするためのヘルパーは通常、GUI ツールキットの python バインディングに含まれており、インポート時に登録される場合があります。IPython には、Matplotlib がサポートするすべての GUI フレームワーク用の入力フック関数も同梱されています。これは 経由でインストールできます%matplotlib。これは、Matplotlib とプロンプトを統合するための推奨される方法です。

IPython / prompt_toolkit #

IPython >= 5.0 では、IPython は CPython の readline ベースのプロンプトの使用からベースのプロンプトに変更されましたprompt_toolkit。 には、メソッドを介して prompt_toolkit 供給される同じ概念的な入力フックがあります。入力フック のソースは にあります。prompt_toolkitIPython.terminal.interactiveshell.TerminalInteractiveShell.inputhook()prompt_toolkitIPython.terminal.pt_inputhooks

脚注