Figuras interativas e programação assíncrona #

Matplotlib suporta figuras interativas ricas incorporando figuras em uma janela GUI. As interações básicas de panorâmica e zoom em um Axes para inspecionar seus dados são 'incorporadas' ao Matplotlib. Isso é suportado por um sistema completo de manipulação de eventos de mouse e teclado que você pode usar para construir gráficos interativos sofisticados.

Este guia pretende ser uma introdução aos detalhes de baixo nível de como funciona a integração do Matplotlib com um loop de eventos da GUI. Para obter uma introdução mais prática à API de eventos do Matplotlib, consulte sistema de manipulação de eventos , Tutorial interativo e Aplicativos interativos usando o Matplotlib .

Loops de eventos #

Fundamentalmente, toda interação do usuário (e rede) é implementada como um loop infinito esperando por eventos do usuário (através do sistema operacional) e fazendo algo a respeito. Por exemplo, um mínimo 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

Faltam muitos detalhes (por exemplo, ele sai na primeira exceção!), mas é representativo dos loops de eventos subjacentes a todos os terminais, GUIs e servidores [ 1 ] . Em geral, a etapa Read está aguardando algum tipo de E/S -- seja entrada do usuário ou da rede -- enquanto Evaluate e Print são responsáveis ​​por interpretar a entrada e fazer algo a respeito.

Na prática, interagimos com um framework que fornece um mecanismo para registrar callbacks a serem executados em resposta a eventos específicos, em vez de implementar diretamente o loop de I/O [ 2 ] . Por exemplo, "quando o usuário clicar neste botão, execute esta função" ou "quando o usuário pressionar a tecla 'z', execute esta outra função". Isso permite que os usuários escrevam programas reativos, orientados a eventos, sem ter que se aprofundar nos detalhes minuciosos [ 3 ] de E/S. O loop de eventos principal às vezes é referido como "o loop principal" e normalmente é iniciado, dependendo da biblioteca, por métodos com nomes como _exec, runou start.

Todos os frameworks GUI (Qt, Wx, Gtk, tk, OSX ou web) possuem algum método para capturar as interações do usuário e passá-las de volta para o aplicativo (por exemplo Signal/ Slotframework no Qt), mas os detalhes exatos dependem do kit de ferramentas. O Matplotlib tem um back- end para cada kit de ferramentas GUI que suportamos, que usa a API do kit de ferramentas para conectar os eventos da IU do kit de ferramentas ao sistema de manipulação de eventos do Matplotlib . Você pode usar FigureCanvasBase.mpl_connectpara conectar sua função ao sistema de manipulação de eventos do Matplotlib. Isso permite que você interaja diretamente com seus dados e escreva interfaces de usuário agnósticas do kit de ferramentas da GUI.

Integração do prompt de comando #

Até agora tudo bem. Temos o REPL (como o terminal IPython) que nos permite enviar código de forma interativa para o interpretador e obter os resultados de volta. Também temos o kit de ferramentas da GUI que executa um loop de eventos aguardando a entrada do usuário e nos permite registrar as funções a serem executadas quando isso acontecer. No entanto, se quisermos fazer as duas coisas, temos um problema: o prompt e o loop de eventos da GUI são loops infinitos que cada um pensa que está no comando! Para que o prompt e as janelas da GUI sejam responsivos, precisamos de um método para permitir que os loops 'compartilhem o tempo':

  1. deixe o loop principal da GUI bloquear o processo python quando quiser janelas interativas

  2. deixe o loop principal da CLI bloquear o processo python e executar intermitentemente o loop da GUI

  3. incorpore totalmente o python na GUI (mas isso é basicamente escrever um aplicativo completo)

Bloqueando o prompt #

pyplot.show

Exibir todas as figuras abertas.

pyplot.pause

Execute o loop de eventos da GUI para intervalos de segundos.

backend_bases.FigureCanvasBase.start_event_loop

Inicie um loop de evento de bloqueio.

backend_bases.FigureCanvasBase.stop_event_loop

Pare o loop de evento de bloqueio atual.

A "integração" mais simples é iniciar o loop de eventos da GUI no modo 'blocking' e assumir a CLI. Enquanto o loop de eventos da GUI está em execução, você não pode inserir novos comandos no prompt (seu terminal pode ecoar os caracteres digitados no terminal, mas eles não serão enviados ao interpretador Python porque ele está ocupado executando o loop de eventos da GUI), mas as janelas da figura serão responsivas. Depois que o loop de eventos for interrompido (deixando qualquer janela de figura ainda aberta sem resposta), você poderá usar o prompt novamente. Reiniciar o loop de eventos tornará qualquer figura aberta responsiva novamente (e processará qualquer interação do usuário na fila).

Para iniciar o loop de eventos até que todas as figuras abertas sejam fechadas, use pyplot.showcomo

pyplot.show(block=True)

Para iniciar o loop de eventos por um período fixo de tempo (em segundos), use pyplot.pause.

Se você não estiver usando pyplot, pode iniciar e parar os loops de eventos via FigureCanvasBase.start_event_loope FigureCanvasBase.stop_event_loop. No entanto, na maioria dos contextos em que você não usaria, pyplotvocê está incorporando o Matplotlib em um aplicativo GUI grande e o loop de eventos da GUI já deve estar em execução para o aplicativo.

Longe do prompt, essa técnica pode ser muito útil se você quiser escrever um script que faça uma pausa para a interação do usuário ou exiba uma figura entre a pesquisa de dados adicionais. Consulte Scripts e funções para obter mais detalhes.

Integração do gancho de entrada #

Embora seja útil executar o loop de eventos da GUI em um modo de bloqueio ou lidar explicitamente com eventos da interface do usuário, podemos fazer melhor! Nós realmente queremos ter um prompt utilizável e janelas de figuras interativas.

Podemos fazer isso usando o recurso 'gancho de entrada' do prompt interativo. Esse gancho é chamado pelo prompt enquanto espera que o usuário digite (mesmo para um digitador rápido, o prompt espera principalmente que o humano pense e mova os dedos). Embora os detalhes variem entre os prompts, a lógica é aproximadamente

  1. começar a esperar pela entrada do teclado

  2. iniciar o loop de eventos da GUI

  3. assim que o usuário pressiona uma tecla, saia do loop de eventos da GUI e manipule a tecla

  4. repetir

Isso nos dá a ilusão de ter simultaneamente janelas GUI interativas e um prompt interativo. Na maioria das vezes, o loop de eventos da GUI está em execução, mas assim que o usuário começa a digitar, o prompt assume novamente.

Essa técnica de compartilhamento de tempo permite apenas que o loop de eventos seja executado enquanto o python estiver ocioso e aguardando a entrada do usuário. Se você deseja que a GUI seja responsiva durante código de execução longa, é necessário liberar periodicamente a fila de eventos da GUI conforme descrito acima . Neste caso, é o seu código, não o REPL, que está bloqueando o processo, então você precisa lidar com o "time-share" manualmente. Por outro lado, um desenho de figura muito lento bloqueará o prompt até que ele termine de desenhar.

Incorporação completa #

Também é possível ir na outra direção e incorporar figuras totalmente (e um interpretador Python ) em um aplicativo nativo rico. O Matplotlib fornece classes para cada kit de ferramentas que podem ser incorporadas diretamente em aplicativos GUI (é assim que as janelas integradas são implementadas!). Consulte Incorporando Matplotlib em interfaces gráficas do usuário para obter mais detalhes.

Scripts e funções #

backend_bases.FigureCanvasBase.flush_events

Descarregue os eventos da GUI para a figura.

backend_bases.FigureCanvasBase.draw_idle

Solicite um redesenho do widget assim que o controle retornar ao loop de eventos da GUI.

figure.Figure.ginput

Chamada de bloqueio para interagir com uma figura.

pyplot.ginput

Chamada de bloqueio para interagir com uma figura.

pyplot.show

Exibir todas as figuras abertas.

pyplot.pause

Execute o loop de eventos da GUI para intervalos de segundos.

Existem vários casos de uso para usar figuras interativas em scripts:

  • capturar a entrada do usuário para orientar o script

  • atualizações de progresso à medida que um script de execução longa progride

  • streaming de atualizações de uma fonte de dados

Funções de bloqueio #

Se você só precisa coletar pontos em um Axes, você pode usar Figure.ginputou, mais geralmente, as ferramentas das blocking_inputferramentas cuidarão de iniciar e parar o loop de eventos para você. No entanto, se você escreveu algum tratamento de evento personalizado ou está usando widgets, precisará executar manualmente o loop de eventos da GUI usando os métodos descritos acima .

Você também pode usar os métodos descritos em Bloqueando o prompt para suspender a execução do loop de eventos da GUI. Assim que o loop sair, seu código será retomado. Em geral, qualquer lugar que você usaria, time.sleepvocê pode usar pyplot.pausecom o benefício adicional de figuras interativas.

Por exemplo, se você deseja pesquisar dados, pode usar algo como

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

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

que pesquisaria novos dados e atualizaria a figura em 1 Hz.

Girando explicitamente o evento Loop #

backend_bases.FigureCanvasBase.flush_events

Descarregue os eventos da GUI para a figura.

backend_bases.FigureCanvasBase.draw_idle

Solicite um redesenho do widget assim que o controle retornar ao loop de eventos da GUI.

Se você tiver janelas abertas com eventos de interface do usuário pendentes (cliques do mouse, pressionamentos de botão ou desenhos), poderá processar explicitamente esses eventos chamando FigureCanvasBase.flush_events. Isso executará o loop de eventos da GUI até que todos os eventos da interface do usuário que estão aguardando tenham sido processados. O comportamento exato depende do back-end, mas normalmente os eventos em toda a figura são processados ​​e apenas os eventos que aguardam para serem processados ​​(não aqueles adicionados durante o processamento) serão tratados.

Por exemplo

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)

Embora isso pareça um pouco lento (já que estamos processando apenas a entrada do usuário a cada 100ms, enquanto 20-30ms é o que parece "responsivo"), ele responderá.

Se você fizer alterações na plotagem e quiser que ela seja renderizada novamente, você precisará chamar draw_idlepara solicitar que a tela seja desenhada novamente. Esse método pode ser pensado como draw_soon em analogia a asyncio.loop.call_soon.

Podemos adicionar isso ao nosso exemplo acima como

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)

Quanto mais vezes você ligar FigureCanvasBase.flush_events, mais responsiva sua figura parecerá, mas ao custo de gastar mais recursos na visualização e menos em sua computação.

Artistas obsoletos #

Artistas (a partir do Matplotlib 1.5) têm um atributo obsoletoTrue que é se o estado interno do artista mudou desde a última vez que foi renderizado. Por padrão, o estado obsoleto é propagado até os pais dos Artistas na árvore de desenho, por exemplo, se a cor de uma Line2D instância for alterada, o Axese Figureque a contém também será marcado como "obsoleto". Assim, fig.staleirá informar se algum artista na figura foi modificado e está fora de sincronia com o que é exibido na tela. Destina-se a ser usado para determinar se draw_idledeve ser chamado para agendar uma nova renderização da figura.

Cada artista possui um Artist.stale_callbackatributo que contém um callback com a assinatura

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

que por padrão é definido como uma função que encaminha o estado obsoleto para o pai do artista. Se você deseja impedir a propagação de um determinado artista, defina este atributo como Nenhum.

Figureas instâncias não têm um artista recipiente e seu callback padrão é None. Se você ligar pyplot.ione não estiver IPython, instalaremos um retorno de chamada para invocar draw_idlesempre que Figureficar obsoleto. Usamos IPythono 'post_execute'gancho para invocar draw_idlequalquer figura obsoleta depois de executar a entrada do usuário, mas antes de retornar o prompt ao usuário. Se você não estiver usando pyplot, pode usar o Figure.stale_callbackatributo de retorno de chamada para ser notificado quando uma figura ficar obsoleta.

Sorteio ocioso #

backend_bases.FigureCanvasBase.draw

Renderize o Figure.

backend_bases.FigureCanvasBase.draw_idle

Solicite um redesenho do widget assim que o controle retornar ao loop de eventos da GUI.

backend_bases.FigureCanvasBase.flush_events

Descarregue os eventos da GUI para a figura.

Em quase todos os casos, recomendamos o uso backend_bases.FigureCanvasBase.draw_idlede over backend_bases.FigureCanvasBase.draw. drawforça uma renderização da figura enquanto draw_idleagenda uma renderização na próxima vez que a janela da GUI for repintar a tela. Isso melhora o desempenho renderizando apenas os pixels que serão mostrados na tela. Se você quiser ter certeza de que a tela será atualizada o mais rápido possível, faça

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

Rosqueamento #

A maioria das estruturas de GUI exige que todas as atualizações na tela e, portanto, seu loop de evento principal sejam executados no thread principal. Isso impossibilita o envio de atualizações periódicas de um gráfico para um thread em segundo plano. Embora pareça retrógrado, normalmente é mais fácil enviar seus cálculos para um thread em segundo plano e atualizar periodicamente a figura no thread principal.

Em geral, o Matplotlib não é thread-safe. Se você for atualizar Artistobjetos em um thread e desenhar em outro, certifique-se de bloquear as seções críticas.

Mecanismo de integração do loop de eventos #

CPython/linha de leitura #

A API Python C fornece um gancho, PyOS_InputHook, para registrar uma função a ser executada ("A função será chamada quando o prompt do interpretador do Python estiver prestes a ficar ocioso e aguardar a entrada do usuário no terminal."). Esse gancho pode ser usado para integrar um segundo loop de eventos (o loop de eventos da GUI) com o loop de prompt de entrada do python. As funções de gancho geralmente esgotam todos os eventos pendentes na fila de eventos da GUI, executam o loop principal por um curto período de tempo fixo ou executam o loop de eventos até que uma tecla seja pressionada em stdin.

Atualmente, o Matplotlib não faz nenhum gerenciamento PyOS_InputHookdevido à ampla variedade de maneiras como o Matplotlib é usado. Esse gerenciamento é deixado para as bibliotecas downstream -- código do usuário ou shell. Figuras interativas, mesmo com Matplotlib em 'modo interativo', podem não funcionar no python repl se um apropriado PyOS_InputHooknão estiver registrado.

Os ganchos de entrada e os auxiliares para instalá-los geralmente são incluídos nas ligações do python para os kits de ferramentas da GUI e podem ser registrados na importação. O IPython também fornece funções de gancho de entrada para todas as estruturas de GUI que o Matplotlib suporta, que podem ser instaladas via %matplotlib. Este é o método recomendado para integrar o Matplotlib e um prompt.

IPython / prompt_toolkit #

Com IPython >= 5.0 IPython mudou de usar o prompt baseado em linha de leitura do CPython para um prompt_toolkitprompt baseado. prompt_toolkit tem o mesmo gancho de entrada conceitual, que é alimentado por prompt_toolkitmeio do IPython.terminal.interactiveshell.TerminalInteractiveShell.inputhook() método. A origem dos prompt_toolkitganchos de entrada reside em IPython.terminal.pt_inputhooks.

notas de rodapé