Interaktive Diagramme und asynchrone Programmierung#
Matplotlib unterstützt reichhaltige interaktive Diagramme durch das Einbetten von Diagrammen in ein GUI-Fenster. Die grundlegenden Interaktionen wie Schwenken und Zoomen in einer Achse zur Untersuchung Ihrer Daten sind sofort verfügbar. Dies wird durch ein vollständiges Maus- und Tastaturereignisbehandlungssystem unterstützt, das Sie zum Erstellen ausgefeilter interaktiver Diagramme verwenden können.
Diese Anleitung soll eine Einführung in die Low-Level-Details der Integration von Matplotlib mit einer GUI-Ereignisschleife geben. Für eine praktischere Einführung in die Matplotlib-Ereignis-API siehe Ereignisbehandlungssystem, Interaktives Tutorial und Interaktive Anwendungen mit Matplotlib.
GUI-Ereignisse#
Alle GUI-Frameworks (Qt, Wx, Gtk, Tk, macOS oder Web) verfügen über eine Methode zur Erfassung von Benutzerinteraktionen und zur Weitergabe an die Anwendung, aber die genauen Details hängen vom Toolkit ab (z. B. Rückrufe in Tk oder das Signal / Slot Framework in Qt). Die Matplotlib Backends kapseln die Details der GUI-Frameworks und bieten über Matplotlibs Ereignisbehandlungssystem eine Framework-unabhängige Schnittstelle zu GUI-Ereignissen. Durch das Verbinden von Funktionen mit dem Ereignisbehandlungssystem (siehe FigureCanvasBase.mpl_connect) können Sie auf benutzergesteuerte Aktionen in einer GUI-Toolkit-unabhängigen Weise reagieren.
Ereignisschleifen#
Grundsätzlich wird die gesamte Benutzerinteraktion (und Netzwerkkommunikation) als eine Endlosschleife implementiert, die auf Ereignisse vom Benutzer (über das Betriebssystem) wartet und dann etwas damit unternimmt. Zum Beispiel ist eine minimale Read-Evaluate-Print-Schleife (REPL):
exec_count = 0
while True:
inp = input(f"[{exec_count}] > ") # Read
ret = eval(inp) # Evaluate
print(ret) # Print
exec_count += 1 # Loop
Dies lässt viele Annehmlichkeiten vermissen (z. B. wird bei der ersten Ausnahme beendet!), ist aber repräsentativ für die Ereignisschleifen, die allen Terminals, GUIs und Servern zugrunde liegen [1]. Im Allgemeinen wartet der Schritt *Read* auf eine Art von I/O - sei es Benutzereingabe oder Netzwerk - während *Evaluate* und *Print* für die Interpretation der Eingabe und dann für das *Tun* damit verantwortlich sind.
In der Praxis interagieren wir mit einem Framework, das einen Mechanismus zur Registrierung von Rückrufen bietet, die als Reaktion auf bestimmte Ereignisse ausgeführt werden, anstatt die I/O-Schleife direkt zu implementieren [2]. Zum Beispiel "wenn der Benutzer auf diesen Button klickt, bitte diese Funktion ausführen" oder "wenn der Benutzer die Taste 'z' drückt, bitte diese andere Funktion ausführen". Dies ermöglicht es Benutzern, reaktive, ereignisgesteuerte Programme zu schreiben, ohne sich mit den Details von I/O befassen zu müssen [3]. Die Kernereignisschleife wird manchmal als "Hauptschleife" bezeichnet und wird typischerweise, abhängig von der Bibliothek, durch Methoden mit Namen wie exec, run oder start gestartet.
Integration mit der Befehlszeile#
Bisher alles gut. Wir haben die REPL (wie das IPython-Terminal), die es uns ermöglicht, interaktiv Code an den Interpreter zu senden und Ergebnisse zurückzubekommen. Wir haben auch das GUI-Toolkit, das eine Ereignisschleife ausführt, die auf Benutzereingaben wartet und es uns ermöglicht, Funktionen zu registrieren, die ausgeführt werden, wenn dies geschieht. Wenn wir jedoch beides tun wollen, haben wir ein Problem: Die Eingabeaufforderung und die GUI-Ereignisschleife sind beide Endlosschleifen und können nicht parallel laufen. Damit sowohl die Eingabeaufforderung als auch die GUI-Fenster reaktionsfähig sind, benötigen wir eine Methode, die es den Schleifen ermöglicht, sich die Zeit zu "teilen".
Blockieren der Eingabeaufforderung: Lassen Sie die GUI-Hauptschleife den Python-Prozess blockieren, wenn Sie interaktive Fenster wünschen.
Integration des Eingabe-Hooks: Lassen Sie die CLI-Hauptschleife den Python-Prozess blockieren und intermittierend die GUI-Schleife ausführen.
Volle Einbettung: Betten Sie Python vollständig in die GUI ein (dies bedeutet jedoch im Grunde, dass Sie eine vollständige Anwendung schreiben).
Blockieren der Eingabeaufforderung#
Zeigt alle geöffneten Plots an. |
|
Führt die GUI-Ereignisschleife für interval Sekunden aus. |
|
Startet eine blockierende Ereignisschleife. |
|
Beendet die aktuelle blockierende Ereignisschleife. |
Die einfachste Lösung ist, die GUI-Ereignisschleife zu starten und sie exklusiv laufen zu lassen, was zu reaktionsfähigen Diagrammfenstern führt. Die CLI-Ereignisschleife wird jedoch nicht ausgeführt, sodass Sie keine neuen Befehle eingeben können. Wir nennen dies "blockierenden" Modus. (Ihr Terminal kann die getippten Zeichen wiederholen, aber sie werden noch nicht von der CLI-Ereignisschleife verarbeitet, da der Python-Interpreter beschäftigt ist, die GUI-Ereignisschleife auszuführen).
Es ist möglich, die GUI-Ereignisschleife zu beenden und die Kontrolle an die CLI-Ereignisschleife zurückzugeben. Sie können dann die Eingabeaufforderung wieder verwenden, aber alle noch geöffneten Diagrammfenster sind nicht mehr reaktionsfähig. Das erneute Starten der GUI-Ereignisschleife macht diese Fenster wieder reaktionsfähig (und verarbeitet alle angesammelten Benutzerinteraktionen).
Der typische Befehl, um alle Diagramme anzuzeigen und die GUI-Ereignisschleife exklusiv auszuführen, bis alle Diagramme geschlossen sind, lautet:
plt.show()
Alternativ können Sie die GUI-Ereignisschleife für eine feste Zeit mit pyplot.pause starten.
Wenn Sie pyplot nicht verwenden, können Sie die Ereignisschleifen über FigureCanvasBase.start_event_loop und FigureCanvasBase.stop_event_loop starten und stoppen. In den meisten Kontexten, in denen Sie pyplot nicht verwenden würden, betten Sie Matplotlib in eine große GUI-Anwendung ein, und die GUI-Ereignisschleife sollte bereits für die Anwendung laufen.
Abseits der Eingabeaufforderung kann diese Technik sehr nützlich sein, wenn Sie ein Skript schreiben möchten, das auf Benutzerinteraktionen wartet oder ein Diagramm zwischen dem Abfragen zusätzlicher Daten anzeigt. Weitere Einzelheiten finden Sie unter Skripte und Funktionen.
Integration des Eingabe-Hooks#
Obwohl das Ausführen der GUI-Ereignisschleife im blockierenden Modus oder die explizite Behandlung von UI-Ereignissen nützlich ist, können wir es noch besser machen! Wir möchten wirklich eine nutzbare Eingabeaufforderung *und* interaktive Diagrammfenster haben.
Dies können wir mit der "Input Hook"-Funktion der interaktiven Eingabeaufforderung erreichen. Dieser Hook wird von der Eingabeaufforderung aufgerufen, während sie auf die Eingabe von Benutzereingaben wartet (auch bei einem schnellen Tipper wartet die Eingabeaufforderung größtenteils darauf, dass der Mensch nachdenkt und seine Finger bewegt). Obwohl die Details zwischen den Eingabeaufforderungen variieren, ist die Logik ungefähr folgende:
Beginnen Sie, auf Tastatureingaben zu warten
Starten Sie die GUI-Ereignisschleife
Sobald der Benutzer eine Taste drückt, beenden Sie die GUI-Ereignisschleife und verarbeiten Sie die Taste.
Wiederholen
Dies gibt uns die Illusion, gleichzeitig interaktive GUI-Fenster und eine interaktive Eingabeaufforderung zu haben. Meistens läuft die GUI-Ereignisschleife, aber sobald der Benutzer zu tippen beginnt, übernimmt die Eingabeaufforderung wieder.
Diese Zeit-Teilungs-Technik erlaubt es der Ereignisschleife nur, wenn Python ansonsten untätig ist und auf Benutzereingaben wartet. Wenn Sie möchten, dass die GUI während lang laufender Codes reaktionsfähig ist, ist es notwendig, die GUI-Ereignisschleife periodisch zu leeren, wie in Explizites Drehen der Ereignisschleife beschrieben. In diesem Fall sind Sie es, der den Prozess blockiert, nicht die REPL, sodass Sie die "Zeitteilung" manuell handhaben müssen. Umgekehrt blockiert eine sehr langsame Diagrammdarstellung die Eingabeaufforderung, bis sie mit dem Zeichnen fertig ist.
Volle Einbettung#
Es ist auch möglich, den umgekehrten Weg zu gehen und Diagramme (und einen Python-Interpreter) vollständig in eine native Anwendung einzubetten. Matplotlib bietet Klassen für jedes Toolkit, die direkt in GUI-Anwendungen eingebettet werden können (so werden die integrierten Fenster implementiert!). Weitere Einzelheiten finden Sie unter Einbetten von Matplotlib in grafische Benutzeroberflächen.
Skripte und Funktionen#
Leert die GUI-Ereignisse für das Diagramm. |
|
Fordert ein Neuzeichnen des Widgets an, sobald die Kontrolle an die GUI-Ereignisschleife zurückgegeben wird. |
|
Blockierender Aufruf zur Interaktion mit einer Abbildung. |
|
Blockierender Aufruf zur Interaktion mit einer Abbildung. |
|
Zeigt alle geöffneten Plots an. |
|
Führt die GUI-Ereignisschleife für interval Sekunden aus. |
Es gibt mehrere Anwendungsfälle für die Verwendung interaktiver Diagramme in Skripten:
Erfassung von Benutzereingaben zur Steuerung des Skripts
Fortschrittsanzeigen bei einem lang laufenden Skript
Streaming-Updates von einer Datenquelle
Blockierende Funktionen#
Wenn Sie nur Punkte in einer Achse sammeln müssen, können Sie Figure.ginput verwenden. Wenn Sie jedoch eine benutzerdefinierte Ereignisbehandlung geschrieben haben oder widgets verwenden, müssen Sie die GUI-Ereignisschleife manuell mit den oben beschriebenen Methoden ausführen.
Sie können auch die Methoden verwenden, die in Blockieren der Eingabeaufforderung beschrieben sind, um die GUI-Ereignisschleife anzuhalten und auszuführen. Sobald die Schleife beendet ist, wird Ihr Code fortgesetzt. Im Allgemeinen können Sie überall dort, wo Sie time.sleep verwenden würden, stattdessen pyplot.pause verwenden, mit dem zusätzlichen Vorteil interaktiver Diagramme.
Zum Beispiel, wenn Sie Daten abfragen möchten, könnten Sie etwas wie Folgendes verwenden:
fig, ax = plt.subplots()
ln, = ax.plot([], [])
while True:
x, y = get_new_data()
ln.set_data(x, y)
plt.pause(1)
Dies würde Daten abfragen und das Diagramm mit 1 Hz aktualisieren.
Explizites Drehen der Ereignisschleife#
Leert die GUI-Ereignisse für das Diagramm. |
|
Fordert ein Neuzeichnen des Widgets an, sobald die Kontrolle an die GUI-Ereignisschleife zurückgegeben wird. |
Wenn Sie offene Fenster haben, die ausstehende UI-Ereignisse (Mausklicks, Tastendrücke oder Zeichnungen) haben, können Sie diese Ereignisse explizit verarbeiten, indem Sie FigureCanvasBase.flush_events aufrufen. Dies führt die GUI-Ereignisschleife aus, bis alle aktuell wartenden UI-Ereignisse verarbeitet wurden. Das genaue Verhalten ist Backend-abhängig, aber typischerweise werden Ereignisse auf allen Diagrammen verarbeitet, und nur Ereignisse, die auf die Verarbeitung warten (nicht solche, die während der Verarbeitung hinzugefügt wurden), werden behandelt.
Zum Beispiel
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)
Während dies etwas träge wirken wird (da wir Benutzereingaben nur alle 100 ms verarbeiten, während 20-30 ms als "reaktionsfähig" empfunden werden), wird es reagieren.
Wenn Sie Änderungen am Plot vornehmen und möchten, dass er neu gerendert wird, müssen Sie draw_idle aufrufen, um ein Neuzeichnen des Canvas anzufordern. Diese Methode kann als *draw_soon* im Vergleich zu asyncio.loop.call_soon betrachtet werden.
Wir können dies unserem obigen Beispiel hinzufügen als
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)
Je öfter Sie FigureCanvasBase.flush_events aufrufen, desto reaktionsfähiger wird Ihr Diagramm wirken, allerdings auf Kosten von mehr Ressourcen für die Visualisierung und weniger für Ihre Berechnung.
Veraltete Künstler#
Künstler haben (seit Matplotlib 1.5) ein Attribut **stale**, das True ist, wenn sich der interne Zustand des Künstlers seit dem letzten Rendern geändert hat. Standardmäßig wird der veraltete Status an die Elternkünstler im Zeichenbaum weitergegeben, z. B. wenn die Farbe einer Line2D-Instanz geändert wird, werden auch die Axes und die Figure, die sie enthalten, als "veraltet" markiert. Somit meldet fig.stale, ob ein Künstler im Diagramm geändert wurde und nicht mehr mit dem auf dem Bildschirm angezeigten übereinstimmt. Dies soll verwendet werden, um zu bestimmen, ob draw_idle aufgerufen werden soll, um ein erneutes Rendern des Diagramms zu planen.
Jeder Künstler hat ein Attribut Artist.stale_callback, das einen Rückruf mit der Signatur enthält
def callback(self: Artist, val: bool) -> None:
...
welches standardmäßig auf eine Funktion gesetzt ist, die den veralteten Status an den Elternkünstler weiterleitet. Wenn Sie verhindern möchten, dass ein bestimmter Künstler weitergegeben wird, setzen Sie dieses Attribut auf None.
Figure-Instanzen haben keinen enthaltenden Künstler und ihr Standardrückruf ist None. Wenn Sie pyplot.ion aufrufen und nicht in IPython sind, installieren wir einen Rückruf, um draw_idle aufzurufen, wann immer die Figure veraltet wird. In IPython verwenden wir den 'post_execute'-Hook, um draw_idle für alle veralteten Diagramme aufzurufen, nachdem die Eingabe des Benutzers ausgeführt wurde, aber bevor die Eingabeaufforderung an den Benutzer zurückgegeben wird. Wenn Sie pyplot nicht verwenden, können Sie das Attribut Figure.stale_callback verwenden, um benachrichtigt zu werden, wenn ein Diagramm veraltet ist.
Leerlaufzeichnung#
Rendert die |
|
Fordert ein Neuzeichnen des Widgets an, sobald die Kontrolle an die GUI-Ereignisschleife zurückgegeben wird. |
|
Leert die GUI-Ereignisse für das Diagramm. |
In fast allen Fällen empfehlen wir die Verwendung von backend_bases.FigureCanvasBase.draw_idle anstelle von backend_bases.FigureCanvasBase.draw. draw erzwingt ein Rendern des Diagramms, während draw_idle ein Rendern plant, wenn das GUI-Fenster das nächste Mal den Bildschirm neu malt. Dies verbessert die Leistung, indem nur Pixel gerendert werden, die auf dem Bildschirm angezeigt werden. Wenn Sie sicherstellen möchten, dass der Bildschirm so schnell wie möglich aktualisiert wird, tun Sie Folgendes:
fig.canvas.draw_idle()
fig.canvas.flush_events()
Threading#
Die meisten GUI-Frameworks verlangen, dass alle Aktualisierungen des Bildschirms und somit ihre Haupt-Ereignisschleife auf dem Hauptthread laufen. Dies macht es unmöglich, periodische Aktualisierungen eines Plots an einen Hintergrund-Thread zu pushen. Obwohl es rückwärts erscheint, ist es typischerweise einfacher, Ihre Berechnungen an einen Hintergrund-Thread zu pushen und das Diagramm periodisch auf dem Hauptthread zu aktualisieren.
Generell ist Matplotlib nicht Thread-sicher. Wenn Sie Artist-Objekte in einem Thread aktualisieren und von einem anderen zeichnen möchten, sollten Sie sicherstellen, dass Sie kritische Abschnitte sperren.
Mechanismus zur Integration der Ereignisschleife#
CPython / readline#
Die Python C-API bietet einen Hook, PyOS_InputHook, um eine Funktion zu registrieren, die ausgeführt werden soll ("Die Funktion wird aufgerufen, wenn die Eingabeaufforderung von Pythons Interpreter kurz vor der Leerlaufstellung steht und auf Benutzereingaben vom Terminal wartet."). Dieser Hook kann verwendet werden, um eine zweite Ereignisschleife (die GUI-Ereignisschleife) mit der Python-Eingabeaufforderungsschleife zu integrieren. Die Hook-Funktionen leeren typischerweise alle anstehenden Ereignisse aus der GUI-Ereignisschleife, führen die Hauptschleife für eine kurze feste Zeit aus oder führen die Ereignisschleife aus, bis eine Taste auf stdin gedrückt wird.
Matplotlib verwaltet derzeit keine PyOS_InputHook aufgrund der Vielzahl von Einsatzmöglichkeiten von Matplotlib. Diese Verwaltung wird nachgelagerten Bibliotheken überlassen - entweder Benutzercode oder der Shell. Interaktive Diagramme, selbst mit Matplotlib im "interaktiven Modus", funktionieren möglicherweise nicht in der Standard-Python-REPL, wenn kein geeigneter PyOS_InputHook registriert ist.
Input Hooks und Helfer zu deren Installation sind normalerweise in den Python-Bindings für GUI-Toolkits enthalten und können beim Import registriert werden. IPython liefert auch Input-Hook-Funktionen für alle von Matplotlib unterstützten GUI-Frameworks, die über %matplotlib installiert werden können. Dies ist die empfohlene Methode zur Integration von Matplotlib und einer Eingabeaufforderung.
IPython / prompt_toolkit#
Mit IPython >= 5.0 hat IPython von der Verwendung der readline-basierten Eingabeaufforderung von CPython zu einer prompt_toolkit-basierten Eingabeaufforderung gewechselt. prompt_toolkit hat denselben konzeptionellen Input Hook, der über die Methode IPython.terminal.interactiveshell.TerminalInteractiveShell.inputhook() in prompt_toolkit eingespeist wird. Der Quellcode für die prompt_toolkit Input Hooks befindet sich unter IPython.terminal.pt_inputhooks.
Fußnoten