Interaktive Figuren und asynchrone Programmierung #

Matplotlib unterstützt reichhaltige interaktive Figuren durch Einbetten von Figuren in ein GUI-Fenster. Die grundlegenden Interaktionen des Schwenkens und Zoomens in einer Achse zum Überprüfen Ihrer Daten sind in Matplotlib „eingebacken“. Dies wird durch ein vollständiges Maus- und Tastatur-Ereignisbehandlungssystem unterstützt, mit dem Sie anspruchsvolle interaktive Diagramme erstellen können.

Dieses Handbuch soll eine Einführung in die grundlegenden Details der Funktionsweise der Matplotlib-Integration mit einer GUI-Ereignisschleife sein. Eine praktischere Einführung in die Matplotlib-Ereignis-API finden Sie unter Ereignisbehandlungssystem , Interaktives Tutorial und Interaktive Anwendungen mit Matplotlib .

Ereignisschleifen #

Grundsätzlich wird jede Benutzerinteraktion (und Vernetzung) als Endlosschleife implementiert, die auf Ereignisse vom Benutzer (über das Betriebssystem) wartet und dann etwas dagegen unternimmt. Beispielsweise ist eine minimale Read Evaluate Print Loop (REPL) möglich

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

Diesem fehlen viele Feinheiten (z. B. wird es bei der ersten Ausnahme beendet!), aber es ist repräsentativ für die Ereignisschleifen, die allen Terminals, GUIs und Servern zugrunde liegen [ 1 ] . Im Allgemeinen wartet der Read -Schritt auf eine Art I/O – sei es Benutzereingabe oder das Netzwerk – während Evaluate und Print dafür verantwortlich sind, die Eingabe zu interpretieren und dann etwas dagegen zu unternehmen.

In der Praxis interagieren wir mit einem Framework, das einen Mechanismus bereitstellt, um Callbacks zu registrieren, die als Reaktion auf bestimmte Ereignisse ausgeführt werden sollen, anstatt die I/O-Schleife direkt zu implementieren [ 2 ] . Zum Beispiel „wenn der Benutzer auf diese Schaltfläche klickt, führen Sie bitte diese Funktion aus“ oder „wenn der Benutzer die Taste „z“ drückt, führen Sie bitte diese andere Funktion aus“. Dies ermöglicht es Benutzern, reaktive, ereignisgesteuerte Programme zu schreiben, ohne sich in die wesentlichen [ 3 ] Details von I/O vertiefen zu müssen. Die zentrale Ereignisschleife wird manchmal als "Hauptschleife" bezeichnet und wird je nach Bibliothek normalerweise von Methoden mit Namen wie _exec, runoder gestartet start.

Alle GUI-Frameworks (Qt, Wx, Gtk, tk, OSX oder Web) verfügen über eine Methode zum Erfassen von Benutzerinteraktionen und deren Rückgabe an die Anwendung (z. B. Signal/ Slotframework in Qt), aber die genauen Details hängen vom Toolkit ab. Matplotlib hat ein Backend für jedes von uns unterstützte GUI-Toolkit, das die Toolkit-API verwendet, um die Toolkit-UI-Ereignisse in das Ereignisbehandlungssystem von Matplotlib zu überbrücken . Sie können dann verwenden FigureCanvasBase.mpl_connect, um Ihre Funktion mit dem Ereignisbehandlungssystem von Matplotlib zu verbinden. Auf diese Weise können Sie direkt mit Ihren Daten interagieren und GUI-Toolkit-agnostische Benutzeroberflächen schreiben.

Eingabeaufforderungsintegration #

So weit, ist es gut. Wir haben die REPL (wie das IPython-Terminal), mit der wir interaktiv Code an den Interpreter senden und Ergebnisse zurückerhalten können. Wir haben auch das GUI-Toolkit, das eine Ereignisschleife ausführt, die auf Benutzereingaben wartet, und uns Funktionen registrieren lässt, die ausgeführt werden sollen, wenn dies geschieht. Wenn wir jedoch beides tun wollen, haben wir ein Problem: Die Eingabeaufforderung und die GUI-Ereignisschleife sind beide Endlosschleifen, von denen jeder glaubt, dass sie verantwortlich sind! Damit sowohl die Eingabeaufforderung als auch die GUI-Fenster reagieren, benötigen wir eine Methode, mit der die Schleifen "timeshare" können:

  1. Lassen Sie die GUI-Hauptschleife den Python-Prozess blockieren, wenn Sie interaktive Fenster wünschen

  2. Lassen Sie die CLI-Hauptschleife den Python-Prozess blockieren und die GUI-Schleife zeitweise ausführen

  3. Python vollständig in die GUI einbetten (aber dies ist im Grunde das Schreiben einer vollständigen Anwendung)

Blockieren der Eingabeaufforderung #

pyplot.show

Alle offenen Zahlen anzeigen.

pyplot.pause

Führen Sie die GUI-Ereignisschleife für Intervallsekunden aus.

backend_bases.FigureCanvasBase.start_event_loop

Starten Sie eine blockierende Ereignisschleife.

backend_bases.FigureCanvasBase.stop_event_loop

Beenden Sie die aktuelle Blockierungsereignisschleife.

Die einfachste "Integration" besteht darin, die GUI-Ereignisschleife im Modus "Blockieren" zu starten und die CLI zu übernehmen. Während die GUI-Ereignisschleife ausgeführt wird, können Sie keine neuen Befehle in die Eingabeaufforderung eingeben (Ihr Terminal gibt möglicherweise die in das Terminal eingegebenen Zeichen wieder, aber sie werden nicht an den Python-Interpreter gesendet, da er damit beschäftigt ist, die GUI-Ereignisschleife auszuführen), aber die Abbildungsfenster reagieren. Sobald die Ereignisschleife beendet ist (wobei alle noch geöffneten Abbildungsfenster nicht mehr reagieren), können Sie die Eingabeaufforderung wieder verwenden. Durch das erneute Starten der Ereignisschleife reagiert jede offene Figur wieder (und verarbeitet alle Benutzerinteraktionen in der Warteschlange).

Um die Ereignisschleife zu starten, bis alle offenen Figuren geschlossen sind, verwenden Sie pyplot.showas

pyplot.show(block=True)

Um die Ereignisschleife für eine festgelegte Zeitspanne (in Sekunden) zu starten, verwenden Sie pyplot.pause.

Wenn Sie es nicht verwenden pyplot, können Sie die Ereignisschleifen über FigureCanvasBase.start_event_loopund starten und stoppen FigureCanvasBase.stop_event_loop. In den meisten Kontexten, in denen Sie Matplotlib jedoch nicht verwenden würden, betten pyplotSie Matplotlib in eine große GUI-Anwendung ein, und die GUI-Ereignisschleife sollte bereits für die Anwendung ausgeführt werden.

Abgesehen von der Eingabeaufforderung kann diese Technik sehr nützlich sein, wenn Sie ein Skript schreiben möchten, das für eine Benutzerinteraktion pausiert oder eine Zahl zwischen dem Abrufen zusätzlicher Daten anzeigt. Weitere Einzelheiten finden Sie unter Skripts und Funktionen .

Input-Hook-Integration #

Während das Ausführen der GUI-Ereignisschleife in einem Blockiermodus oder das explizite Behandeln von UI-Ereignissen nützlich ist, können wir es besser machen! Wir möchten wirklich in der Lage sein, eine brauchbare Eingabeaufforderung und interaktive Figurenfenster zu haben.

Wir können dies mit der Funktion „Eingabehaken“ der interaktiven Eingabeaufforderung tun. Dieser Hook wird von der Eingabeaufforderung aufgerufen, während sie darauf wartet, dass der Benutzer tippt (selbst für eine schnelle Schreibkraft wartet die Eingabeaufforderung meistens darauf, dass der Mensch nachdenkt und seine Finger bewegt). Obwohl die Details zwischen den Eingabeaufforderungen variieren, ist die Logik grob

  1. Beginnen Sie, auf Tastatureingaben zu warten

  2. Starten Sie die GUI-Ereignisschleife

  3. Sobald der Benutzer eine Taste drückt, verlassen Sie die GUI-Ereignisschleife und behandeln Sie die Taste

  4. wiederholen

Dies gibt uns die Illusion, gleichzeitig interaktive GUI-Fenster und eine interaktive Eingabeaufforderung zu haben. Die meiste Zeit läuft die GUI-Ereignisschleife, aber sobald der Benutzer mit der Eingabe beginnt, übernimmt die Eingabeaufforderung wieder.

Diese Timesharing-Technik lässt die Ausführung der Ereignisschleife nur zu, während Python ansonsten im Leerlauf ist und auf Benutzereingaben wartet. Wenn Sie möchten , dass die GUI während langer Codeausführung reagiert , ist es notwendig , die GUI - Ereigniswarteschlange wie oben beschrieben regelmäßig zu leeren . In diesem Fall ist es Ihr Code, nicht die REPL, die den Prozess blockiert, sodass Sie den "Time-Share" manuell handhaben müssen. Umgekehrt blockiert ein sehr langsames Zeichnen einer Figur die Eingabeaufforderung, bis das Zeichnen abgeschlossen ist.

Vollständige Einbettung #

Es ist auch möglich, in die andere Richtung zu gehen und Figuren (und einen Python-Interpreter ) vollständig in eine reichhaltige native Anwendung einzubetten. Matplotlib bietet Klassen für jedes Toolkit, die direkt in GUI-Anwendungen eingebettet werden können (so werden die eingebauten Fenster implementiert!). Weitere Einzelheiten finden Sie unter Einbetten von Matplotlib in grafische Benutzeroberflächen .

Skripte und Funktionen #

backend_bases.FigureCanvasBase.flush_events

Leeren Sie die GUI-Ereignisse für die Figur.

backend_bases.FigureCanvasBase.draw_idle

Fordern Sie eine Widget-Neuzeichnung an, sobald die Steuerung zur GUI-Ereignisschleife zurückkehrt.

figure.Figure.ginput

Anruf blockieren, um mit einer Figur zu interagieren.

pyplot.ginput

Anruf blockieren, um mit einer Figur zu interagieren.

pyplot.show

Alle offenen Zahlen anzeigen.

pyplot.pause

Führen Sie die GUI-Ereignisschleife für Intervallsekunden aus.

Es gibt mehrere Anwendungsfälle für die Verwendung interaktiver Figuren in Skripten:

  • Erfassen Sie Benutzereingaben, um das Skript zu steuern

  • Fortschrittsaktualisierungen, wenn ein lange laufendes Skript fortschreitet

  • Streaming-Updates von einer Datenquelle

Sperrfunktionen #

Wenn Sie nur Punkte in einer Achse sammeln müssen, können Sie Figure.ginputdie Tools von blocking_inputTools verwenden, um das Starten und Stoppen der Ereignisschleife für Sie zu erledigen. Wenn Sie jedoch eine benutzerdefinierte Ereignisbehandlung geschrieben haben oder verwenden widgets, müssen Sie die GUI-Ereignisschleife mit den oben beschriebenen Methoden manuell ausführen .

Sie können auch die unter Blockieren der Eingabeaufforderung beschriebenen Methoden verwenden , um die Ausführung der GUI-Ereignisschleife auszusetzen. Sobald die Schleife beendet wird, wird Ihr Code fortgesetzt. Im Allgemeinen können Sie stattdessen jeden Ort verwenden, den Sie time.sleepverwenden würden, pyplot.pausemit dem zusätzlichen Vorteil interaktiver Figuren.

Wenn Sie beispielsweise Daten abfragen möchten, können Sie so etwas wie verwenden

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

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

die nach neuen Daten abfragen und die Zahl bei 1 Hz aktualisieren würde.

Explizites Drehen des Events Loop #

backend_bases.FigureCanvasBase.flush_events

Leeren Sie die GUI-Ereignisse für die Figur.

backend_bases.FigureCanvasBase.draw_idle

Fordern Sie eine Widget-Neuzeichnung an, sobald die Steuerung zur GUI-Ereignisschleife zurückkehrt.

Wenn Sie offene Fenster mit ausstehenden UI-Ereignissen (Mausklicks, Tastendruck oder Ziehen) haben, können Sie diese Ereignisse explizit verarbeiten, indem Sie aufrufen FigureCanvasBase.flush_events. Dadurch wird die GUI-Ereignisschleife ausgeführt, bis alle derzeit wartenden UI-Ereignisse verarbeitet wurden. Das genaue Verhalten ist vom Backend abhängig, aber normalerweise werden Ereignisse auf allen Zahlen verarbeitet und nur Ereignisse, die darauf warten, verarbeitet zu werden (nicht die während der Verarbeitung hinzugefügten), 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)

Dies wird sich zwar etwas träge anfühlen (da wir Benutzereingaben nur alle 100 ms verarbeiten, während sich 20-30 ms "reaktionsschnell" anfühlen), reagiert es jedoch.

Wenn Sie Änderungen an der Handlung vornehmen und diese neu gerendert haben möchten, müssen Sie anrufen draw_idle, um anzufordern, dass die Leinwand neu gezeichnet wird. Diese Methode kann man sich in Analogie zu draw_soon vorstellenasyncio.loop.call_soon .

Wir können dies zu 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 häufiger Sie anrufen, FigureCanvasBase.flush_eventsdesto reaktionsschneller wird sich Ihre Figur anfühlen, aber auf Kosten von mehr Ressourcen für die Visualisierung und weniger für Ihre Berechnung.

Veraltete Künstler #

Künstler (ab Matplotlib 1.5) haben ein veraltetes Attribut, das heißt, Truewenn sich der interne Zustand des Künstlers seit dem letzten Mal geändert hat, als er gerendert wurde. Standardmäßig wird der veraltete Zustand bis zu den übergeordneten Künstlern im Zeichnungsbaum weitergegeben, zB wenn die Farbe einer Line2D Instanz geändert wird, werden die Axesund Figuredie sie enthalten ebenfalls als "veraltet" markiert. Daher fig.stalewird gemeldet, wenn ein Künstler in der Figur geändert wurde und nicht mehr mit dem übereinstimmt, was auf dem Bildschirm angezeigt wird. Dies soll verwendet werden, um zu bestimmen, ob draw_idleaufgerufen werden sollte, um ein erneutes Rendern der Figur zu planen.

Jeder Künstler hat ein Artist.stale_callbackAttribut, das einen Rückruf mit der Signatur enthält

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

die standardmäßig auf eine Funktion eingestellt ist, die den veralteten Zustand an das übergeordnete Element des Künstlers weiterleitet. Wenn Sie die Verbreitung eines bestimmten Künstlers unterdrücken möchten, setzen Sie dieses Attribut auf „Keine“.

FigureInstanzen haben keinen enthaltenden Künstler und ihr Standardrückruf ist None. Wenn Sie anrufen pyplot.ionund nicht dabei sind IPython, installieren wir einen Rückruf, der aufgerufen wird, draw_idlewenn der Figureveraltet wird. In IPythonverwenden wir den 'post_execute'Hook, um draw_idleveraltete Zahlen aufzurufen, nachdem wir die Eingabe des Benutzers ausgeführt haben, aber bevor wir die Eingabeaufforderung an den Benutzer zurückgeben. Wenn Sie es nicht verwenden pyplot, können Sie das Callback Figure.stale_callback-Attribut verwenden, um benachrichtigt zu werden, wenn eine Figur veraltet ist.

Leerlauf ziehen #

backend_bases.FigureCanvasBase.draw

Rendern Sie die Figure.

backend_bases.FigureCanvasBase.draw_idle

Fordern Sie eine Widget-Neuzeichnung an, sobald die Steuerung zur GUI-Ereignisschleife zurückkehrt.

backend_bases.FigureCanvasBase.flush_events

Leeren Sie die GUI-Ereignisse für die Figur.

In fast allen Fällen empfehlen wir die Verwendung von backend_bases.FigureCanvasBase.draw_idleüber backend_bases.FigureCanvasBase.draw. drawerzwingt ein Rendern der Figur, während draw_idleein Rendern geplant wird, wenn das GUI-Fenster das nächste Mal den Bildschirm neu zeichnet. Dies verbessert die Leistung, indem nur Pixel gerendert werden, die auf dem Bildschirm angezeigt werden. Wenn Sie sicher sein möchten, dass der Bildschirm so schnell wie möglich aktualisiert wird, tun Sie dies

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

Einfädeln #

Die meisten GUI-Frameworks erfordern, dass alle Aktualisierungen des Bildschirms und damit ihre Hauptereignisschleife im Hauptthread ausgeführt werden. Dies macht es unmöglich, regelmäßige Aktualisierungen eines Diagramms an einen Hintergrundthread zu senden. Obwohl es rückwärts scheint, ist es in der Regel einfacher, Ihre Berechnungen in einen Hintergrundthread zu verschieben und die Abbildung regelmäßig im Hauptthread zu aktualisieren.

Im Allgemeinen ist Matplotlib nicht Thread-sicher. Wenn Sie Objekte in einem Thread aktualisieren Artistund aus einem anderen ziehen, sollten Sie sicherstellen, dass Sie die kritischen Abschnitte sperren.

Eventloop-Integrationsmechanismus #

CPython / readline #

Die Python-C-API bietet einen Hook, PyOS_InputHook, um eine auszuführende Funktion zu registrieren ("Die Funktion wird aufgerufen, wenn Pythons Interpreter-Eingabeaufforderung im Begriff ist, inaktiv zu werden und auf Benutzereingaben vom Terminal zu warten."). Dieser Hook kann verwendet werden, um eine zweite Ereignisschleife (die GUI-Ereignisschleife) in die Python-Eingabeaufforderungsschleife zu integrieren. Die Hook-Funktionen erschöpfen normalerweise alle anstehenden Ereignisse in der GUI-Ereigniswarteschlange, führen die Hauptschleife für eine kurze festgelegte Zeitspanne aus oder führen die Ereignisschleife aus, bis eine Taste auf stdin gedrückt wird.

Matplotlib führt derzeit keine Verwaltung PyOS_InputHookdurch, da Matplotlib auf vielfältige Weise verwendet wird. Diese Verwaltung wird nachgelagerten Bibliotheken überlassen – entweder dem Benutzercode oder der Shell. Interaktive Figuren, selbst mit Matplotlib im „interaktiven Modus“, funktionieren möglicherweise nicht in der Vanilla-Python-Repl, wenn keine entsprechende PyOS_InputHookregistriert ist.

Eingabe-Hooks und Helfer zu ihrer Installation sind normalerweise in den Python-Bindungen für GUI-Toolkits enthalten und können beim Import registriert werden. IPython liefert auch Eingabe-Hook-Funktionen für alle von Matplotlib unterstützten GUI-Frameworks, die über installiert werden können %matplotlib. Dies ist die empfohlene Methode zur Integration von Matplotlib und einer Eingabeaufforderung.

IPython / prompt_toolkit #

Mit IPython >= 5.0 hat sich IPython von der Verwendung der Readline-basierten Eingabeaufforderung von CPython zu einer prompt_toolkitbasierten Eingabeaufforderung geändert. prompt_toolkit hat denselben konzeptionellen Input-Hook, in prompt_toolkitden über die IPython.terminal.interactiveshell.TerminalInteractiveShell.inputhook() Methode eingespeist wird. Die Quelle für die prompt_toolkitInput-Hooks befindet sich unter IPython.terminal.pt_inputhooks.

Fußnoten