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, runou 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/ Slotframework 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_connectpour 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" :

  1. laissez la boucle principale de l'interface graphique bloquer le processus python lorsque vous voulez des fenêtres interactives

  2. laisser la boucle principale CLI bloquer le processus python et exécuter par intermittence la boucle GUI

  3. intégrer entièrement python dans l'interface graphique (mais il s'agit essentiellement d'écrire une application complète)

Bloquer l'invite #

pyplot.show

Afficher tous les chiffres ouverts.

pyplot.pause

Exécutez la boucle d'événements de l'interface graphique pendant des secondes d' intervalle .

backend_bases.FigureCanvasBase.start_event_loop

Démarrer une boucle d'événements bloquants.

backend_bases.FigureCanvasBase.stop_event_loop

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.showcomme

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_loopet FigureCanvasBase.stop_event_loop. Cependant, dans la plupart des contextes où vous n'utiliseriez pas, pyplotvous 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

  1. commencer à attendre la saisie au clavier

  2. démarrer la boucle d'événements de l'interface graphique

  3. dès que l'utilisateur appuie sur une touche, quittez la boucle d'événements de l'interface graphique et gérez la touche

  4. 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 #

backend_bases.FigureCanvasBase.flush_events

Videz les événements GUI pour la figure.

backend_bases.FigureCanvasBase.draw_idle

Demander un redessin du widget une fois que le contrôle revient à la boucle d'événements de l'interface graphique.

figure.Figure.ginput

Appel bloquant pour interagir avec une figurine.

pyplot.ginput

Appel bloquant pour interagir avec une figurine.

pyplot.show

Afficher tous les chiffres ouverts.

pyplot.pause

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.ginputou plus généralement les outils des blocking_inputoutils 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.sleeppeut ê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 #

backend_bases.FigureCanvasBase.flush_events

Videz les événements GUI pour la figure.

backend_bases.FigureCanvasBase.draw_idle

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_idlepour 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_eventsplus 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 Axeset Figurequi la contiennent seront également marqués comme "obsolètes". Ainsi, fig.stalesignalera 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_idledoit être appelé pour planifier un nouveau rendu de la figure.

Chaque artiste a un Artist.stale_callbackattribut 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.

Figureles instances n'ont pas d'artiste conteneur et leur rappel par défaut est None. Si vous appelez pyplot.ionet que vous n'êtes pas IPythonprésent, nous installerons un rappel à invoquer draw_idlechaque fois que le Figuredevient obsolète. Dans IPythonnous utilisons le 'post_execute'hook pour invoquer draw_idlen'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_callbackpour être averti lorsqu'un chiffre est devenu obsolète.

Tirage inactif #

backend_bases.FigureCanvasBase.draw

Rendre le Figure.

backend_bases.FigureCanvasBase.draw_idle

Demander un redessin du widget une fois que le contrôle revient à la boucle d'événements de l'interface graphique.

backend_bases.FigureCanvasBase.flush_events

Videz les événements GUI pour la figure.

Dans presque tous les cas, nous vous recommandons d'utiliser backend_bases.FigureCanvasBase.draw_idleplus de backend_bases.FigureCanvasBase.draw. drawforce un rendu de la figure alors draw_idlequ'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 Artistdes 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_InputHookraison 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_InputHookn'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_toolkitinvite basée. prompt_toolkit a le même crochet d'entrée conceptuel, qui est alimenté prompt_toolkitvia la IPython.terminal.interactiveshell.TerminalInteractiveShell.inputhook() méthode. La source prompt_toolkitdes crochets d'entrée se trouve à IPython.terminal.pt_inputhooks.

Notes de bas de page