Figures interactives et programmation asynchrone #
Matplotlib prend en charge des figures interactives riches en incorporant des figures dans une fenêtre GUI. Les interactions de base du panoramique et du zoom dans un Axes pour inspecter vos données sont "intégrées" à Matplotlib. Ceci est pris en charge par un système complet de gestion des événements de souris et de clavier que vous pouvez utiliser pour créer des graphiques interactifs sophistiqués.
Ce guide est censé être une introduction aux détails de bas niveau du fonctionnement de l'intégration de Matplotlib avec une boucle d'événement GUI. Pour une introduction plus pratique à l'API d'événements Matplotlib, voir Système de gestion d'événements , Tutoriel interactif et Applications interactives utilisant Matplotlib .
Boucles d'événements #
Fondamentalement, toute interaction utilisateur (et mise en réseau) est implémentée sous la forme d'une boucle infinie attendant les événements de l'utilisateur (via le système d'exploitation), puis faisant quelque chose à ce sujet. Par exemple, une boucle minimale de lecture, d'évaluation et d'impression (REPL) est
exec_count = 0
while True:
inp = input(f"[{exec_count}] > ") # Read
ret = eval(inp) # Evaluate
print(ret) # Print
exec_count += 1 # Loop
Cela manque de nombreuses subtilités (par exemple, il se termine à la première exception !), mais est représentatif des boucles d'événements qui sous-tendent tous les terminaux, interfaces graphiques et serveurs [ 1 ] . En général, l' étape de lecture attend une sorte d'E/S - qu'il s'agisse d'une entrée utilisateur ou du réseau - tandis que l' évaluation et l' impression sont responsables de l'interprétation de l'entrée, puis de l'action .
En pratique, nous interagissons avec un cadre qui fournit un mécanisme pour enregistrer les rappels à exécuter en réponse à des événements spécifiques plutôt que d'implémenter directement la boucle d'E/S [ 2 ] . Par exemple "lorsque l'utilisateur clique sur ce bouton, veuillez exécuter cette fonction" ou "lorsque l'utilisateur appuie sur la touche 'z', veuillez exécuter cette autre fonction". Cela permet aux utilisateurs d'écrire des programmes réactifs, pilotés par des événements, sans avoir à se plonger dans les moindres détails [ 3 ] des E/S. La boucle d'événements principale est parfois appelée "boucle principale" et est généralement démarrée, selon la bibliothèque, par des méthodes portant des noms tels que _exec
,
run
ou start
.
Tous les frameworks GUI (Qt, Wx, Gtk, tk, OSX ou web) ont une méthode pour capturer les interactions de l'utilisateur et les renvoyer à l'application (par exemple Signal
/ Slot
framework dans Qt) mais les détails exacts dépendent de la boîte à outils. Matplotlib a un backend pour chaque boîte à outils GUI que nous prenons en charge qui utilise l'API de la boîte à outils pour relier les événements de l'interface utilisateur de la boîte à outils au système de gestion des événements de Matplotlib . Vous pouvez ensuite utiliser
FigureCanvasBase.mpl_connect
pour connecter votre fonction au système de gestion des événements de Matplotlib. Cela vous permet d'interagir directement avec vos données et d'écrire des interfaces utilisateur indépendantes de la boîte à outils GUI.
Intégration de l'invite de commande #
Jusqu'ici tout va bien. Nous avons le REPL (comme le terminal IPython) qui nous permet d'envoyer du code de manière interactive à l'interpréteur et de récupérer les résultats. Nous avons également la boîte à outils de l'interface graphique qui exécute une boucle d'événements en attente de l'entrée de l'utilisateur et nous permet d'enregistrer les fonctions à exécuter lorsque cela se produit. Cependant, si nous voulons faire les deux, nous avons un problème : l'invite et la boucle d'événements de l'interface graphique sont toutes deux des boucles infinies dont chacune pense qu'elles sont en charge ! Pour que l'invite et les fenêtres de l'interface graphique soient réactives, nous avons besoin d'une méthode permettant aux boucles de "timeshare" :
laissez la boucle principale de l'interface graphique bloquer le processus python lorsque vous voulez des fenêtres interactives
laisser la boucle principale CLI bloquer le processus python et exécuter par intermittence la boucle GUI
intégrer entièrement python dans l'interface graphique (mais il s'agit essentiellement d'écrire une application complète)
Bloquer l'invite #
Afficher tous les chiffres ouverts. |
|
Exécutez la boucle d'événements de l'interface graphique pendant des secondes d' intervalle . |
|
Démarrer une boucle d'événements bloquants. |
|
Arrête la boucle d'événements de blocage en cours. |
L'"intégration" la plus simple consiste à démarrer la boucle d'événements de l'interface graphique en mode "bloquant" et à prendre en charge la CLI. Pendant que la boucle d'événement GUI est en cours d'exécution, vous ne pouvez pas entrer de nouvelles commandes dans l'invite (votre terminal peut renvoyer les caractères saisis dans le terminal, mais ils ne seront pas envoyés à l'interpréteur Python car il est occupé à exécuter la boucle d'événement GUI), mais les fenêtres de chiffres seront réactives. Une fois la boucle d'événements arrêtée (laissant toutes les fenêtres de figure encore ouvertes non réactives), vous pourrez à nouveau utiliser l'invite. Le redémarrage de la boucle d'événements rendra à nouveau réactif tout personnage ouvert (et traitera toute interaction utilisateur en file d'attente).
Pour démarrer la boucle d'événements jusqu'à ce que toutes les figures ouvertes soient fermées, utilisez
pyplot.show
comme
pyplot.show(block=True)
Pour démarrer la boucle d'événements pendant une durée fixe (en secondes), utilisez
pyplot.pause
.
Si vous n'utilisez pas pyplot
, vous pouvez démarrer et arrêter les boucles d'événements via FigureCanvasBase.start_event_loop
et
FigureCanvasBase.stop_event_loop
. Cependant, dans la plupart des contextes où vous n'utiliseriez pas, pyplot
vous intégrez Matplotlib dans une grande application GUI et la boucle d'événement GUI devrait déjà être en cours d'exécution pour l'application.
Loin de l'invite, cette technique peut être très utile si vous souhaitez écrire un script qui s'interrompt pour l'interaction de l'utilisateur ou affiche un chiffre entre les interrogations pour des données supplémentaires. Voir Scripts et fonctions pour plus de détails.
Intégration du crochet d'entrée #
Bien qu'il soit utile d'exécuter la boucle d'événements de l'interface graphique en mode bloquant ou de gérer explicitement les événements de l'interface utilisateur, nous pouvons faire mieux ! Nous voulons vraiment pouvoir avoir une invite utilisable et des fenêtres de figures interactives.
Nous pouvons le faire en utilisant la fonction "crochet d'entrée" de l'invite interactive. Ce crochet est appelé par l'invite pendant qu'il attend que l'utilisateur tape (même pour un dactylographe rapide, l'invite attend principalement que l'humain réfléchisse et bouge ses doigts). Bien que les détails varient d'une invite à l'autre, la logique est à peu près
commencer à attendre la saisie au clavier
démarrer la boucle d'événements de l'interface graphique
dès que l'utilisateur appuie sur une touche, quittez la boucle d'événements de l'interface graphique et gérez la touche
répéter
Cela nous donne l'illusion d'avoir simultanément des fenêtres GUI interactives et une invite interactive. La plupart du temps, la boucle d'événements de l'interface graphique est en cours d'exécution, mais dès que l'utilisateur commence à taper, l'invite reprend le dessus.
Cette technique de temps partagé permet uniquement à la boucle d'événements de s'exécuter pendant que python est autrement inactif et attend l'entrée de l'utilisateur. Si vous souhaitez que l'interface graphique soit réactive lors d'un long code d'exécution, il est nécessaire de vider périodiquement la file d'attente des événements de l'interface graphique comme décrit ci- dessus . Dans ce cas, c'est votre code, et non le REPL, qui bloque le processus, vous devez donc gérer le "time-share" manuellement. Inversement, un dessin de figure très lent bloquera l'invite jusqu'à ce qu'il finisse de dessiner.
Intégration complète #
Il est également possible d'aller dans l'autre sens et d'embarquer entièrement des figures (et un interpréteur Python ) dans une application native riche. Matplotlib fournit des classes pour chaque boîte à outils qui peuvent être directement intégrées dans des applications graphiques (c'est ainsi que les fenêtres intégrées sont implémentées !). Voir Intégration de Matplotlib dans les interfaces utilisateur graphiques pour plus de détails.
Scripts et fonctions #
Videz les événements GUI pour la figure. |
|
Demander un redessin du widget une fois que le contrôle revient à la boucle d'événements de l'interface graphique. |
|
Appel bloquant pour interagir avec une figurine. |
|
Appel bloquant pour interagir avec une figurine. |
|
Afficher tous les chiffres ouverts. |
|
Exécutez la boucle d'événements de l'interface graphique pendant des secondes d' intervalle . |
Il existe plusieurs cas d'utilisation pour utiliser des figures interactives dans des scripts :
capturer l'entrée de l'utilisateur pour piloter le script
mises à jour de progression au fur et à mesure de la progression d'un long script
diffusion en continu de mises à jour à partir d'une source de données
Fonctions de blocage #
Si vous n'avez besoin que de collecter des points dans un Axes vous pouvez utiliser
Figure.ginput
ou plus généralement les outils des
blocking_input
outils se chargeront de démarrer et d'arrêter la boucle événementielle à votre place. Cependant, si vous avez écrit une gestion d'événement personnalisée ou si vous l'utilisez widgets
, vous devrez exécuter manuellement la boucle d'événement de l'interface graphique en utilisant les méthodes décrites ci- dessus .
Vous pouvez également utiliser les méthodes décrites dans Blocage de l'invite
pour suspendre l'exécution de la boucle d'événements de l'interface graphique. Une fois la boucle terminée, votre code reprendra. En général, n'importe quel endroit que vous utiliseriez time.sleep
peut être utilisé
pyplot.pause
à la place avec l'avantage supplémentaire de figures interactives.
Par exemple, si vous souhaitez interroger des données, vous pouvez utiliser quelque chose comme
fig, ax = plt.subplots()
ln, = ax.plot([], [])
while True:
x, y = get_new_data()
ln.set_data(x, y)
plt.pause(1)
qui interrogerait pour de nouvelles données et mettrait à jour le chiffre à 1Hz.
Faire tourner explicitement l'événement Loop #
Videz les événements GUI pour la figure. |
|
Demander un redessin du widget une fois que le contrôle revient à la boucle d'événements de l'interface graphique. |
Si vous avez des fenêtres ouvertes qui ont des événements d'interface utilisateur en attente (clics de souris, pressions sur des boutons ou tirages), vous pouvez traiter explicitement ces événements en appelant FigureCanvasBase.flush_events
. Cela exécutera la boucle d'événements de l'interface graphique jusqu'à ce que tous les événements de l'interface utilisateur en attente aient été traités. Le comportement exact dépend du backend, mais généralement, les événements sur tous les chiffres sont traités et seuls les événements en attente de traitement (pas ceux ajoutés pendant le traitement) seront traités.
Par exemple
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)
Bien que cela semble un peu lent (car nous ne traitons que les entrées de l'utilisateur toutes les 100 ms alors que 20 à 30 ms est ce qui semble "réactif"), il répondra.
Si vous apportez des modifications au tracé et souhaitez qu'il soit restitué, vous devrez appeler draw_idle
pour demander que le canevas soit redessiné. Cette méthode peut être considérée comme draw_soon par analogie avec
asyncio.loop.call_soon
.
Nous pouvons ajouter ceci à notre exemple ci-dessus comme
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)
Plus vous appelez fréquemment, FigureCanvasBase.flush_events
plus votre personnage se sentira réactif, mais au prix de dépenser plus de ressources sur la visualisation et moins sur votre calcul.
Artistes obsolètes #
Les artistes (à partir de Matplotlib 1.5) ont un attribut
périméTrue
qui indique si l'état interne de l'artiste a changé depuis la dernière fois qu'il a été rendu. Par défaut, l'état obsolète est propagé jusqu'aux parents Artistes dans l'arbre de dessin, par exemple, si la couleur d'une Line2D
instance est modifiée, les Axes
et Figure
qui la contiennent seront également marqués comme "obsolètes". Ainsi, fig.stale
signalera si un artiste de la figure a été modifié et n'est pas synchronisé avec ce qui est affiché à l'écran. Ceci est destiné à être utilisé pour déterminer s'il draw_idle
doit être appelé pour planifier un nouveau rendu de la figure.
Chaque artiste a un Artist.stale_callback
attribut qui contient un callback avec la signature
def callback(self: Artist, val: bool) -> None:
...
qui est défini par défaut sur une fonction qui transmet l'état obsolète au parent de l'artiste. Si vous souhaitez empêcher un artiste donné de se propager, définissez cet attribut sur Aucun.
Figure
les instances n'ont pas d'artiste conteneur et leur rappel par défaut est None
. Si vous appelez pyplot.ion
et que vous n'êtes pas
IPython
présent, nous installerons un rappel à invoquer
draw_idle
chaque fois que le
Figure
devient obsolète. Dans IPython
nous utilisons le
'post_execute'
hook pour invoquer
draw_idle
n'importe quel chiffre obsolète après avoir exécuté l'entrée de l'utilisateur, mais avant de renvoyer l'invite à l'utilisateur. Si vous n'utilisez pas pyplot
, vous pouvez utiliser l'attribut de rappel
Figure.stale_callback
pour être averti lorsqu'un chiffre est devenu obsolète.
Tirage inactif #
Rendre le |
|
Demander un redessin du widget une fois que le contrôle revient à la boucle d'événements de l'interface graphique. |
|
Videz les événements GUI pour la figure. |
Dans presque tous les cas, nous vous recommandons d'utiliser
backend_bases.FigureCanvasBase.draw_idle
plus de
backend_bases.FigureCanvasBase.draw
. draw
force un rendu de la figure alors draw_idle
qu'il planifie un rendu la prochaine fois que la fenêtre de l'interface graphique va repeindre l'écran. Cela améliore les performances en rendant uniquement les pixels qui seront affichés à l'écran. Si vous voulez être sûr que l'écran est mis à jour dès que possible, faites
fig.canvas.draw_idle()
fig.canvas.flush_events()
Enfilage #
La plupart des frameworks GUI exigent que toutes les mises à jour de l'écran, et donc leur boucle d'événement principale, s'exécutent sur le thread principal. Cela rend impossible de pousser les mises à jour périodiques d'un tracé vers un thread d'arrière-plan. Bien que cela semble inversé, il est généralement plus facile de pousser vos calculs vers un thread d'arrière-plan et de mettre à jour périodiquement la figure sur le thread principal.
En général, Matplotlib n'est pas thread-safe. Si vous allez mettre à jour
Artist
des objets dans un thread et dessiner à partir d'un autre, vous devez vous assurer que vous verrouillez les sections critiques.
Mécanisme d'intégration Eventloop #
CPython / readline #
L'API Python C fournit un crochet, PyOS_InputHook
, pour enregistrer une fonction à exécuter ("La fonction sera appelée lorsque l'invite de l'interpréteur de Python est sur le point de devenir inactive et attend l'entrée de l'utilisateur depuis le terminal."). Ce crochet peut être utilisé pour intégrer une deuxième boucle d'événement (la boucle d'événement GUI) avec la boucle d'invite d'entrée python. Les fonctions de crochet épuisent généralement tous les événements en attente dans la file d'attente des événements de l'interface graphique, exécutent la boucle principale pendant une courte durée fixe ou exécutent la boucle d'événements jusqu'à ce qu'une touche soit enfoncée sur stdin.
Matplotlib ne fait actuellement aucune gestion en PyOS_InputHook
raison du large éventail de façons dont Matplotlib est utilisé. Cette gestion est laissée aux bibliothèques en aval - soit le code utilisateur, soit le shell. Les figures interactives, même avec Matplotlib en 'mode interactif', peuvent ne pas fonctionner dans le repl vanilla python si un approprié PyOS_InputHook
n'est pas enregistré.
Les hooks d'entrée et les assistants pour les installer sont généralement inclus avec les liaisons python pour les kits d'outils GUI et peuvent être enregistrés lors de l'importation. IPython fournit également des fonctions de crochet d'entrée pour tous les frameworks d'interface graphique pris en charge par Matplotlib qui peuvent être installés via %matplotlib
. Il s'agit de la méthode recommandée pour intégrer Matplotlib et une invite.
IPython / prompt_toolkit #
Avec IPython>= 5.0, IPython est passé de l'utilisation de l'invite basée sur la ligne de lecture de CPython à une prompt_toolkit
invite basée. prompt_toolkit
a le même crochet d'entrée conceptuel, qui est alimenté prompt_toolkit
via la
IPython.terminal.interactiveshell.TerminalInteractiveShell.inputhook()
méthode. La source prompt_toolkit
des crochets d'entrée se trouve à
IPython.terminal.pt_inputhooks
.
Notes de bas de page