交互式图形和异步编程#

Matplotlib 通过将图形嵌入 GUI 窗口来支持丰富的交互式图形。平移和缩放轴以检查数据的基本交互已“嵌入”到 Matplotlib。这是由一个完整的鼠标和键盘事件处理系统支持的,您可以使用它来构建复杂的交互式图表。

本指南旨在介绍 Matplotlib 如何与 GUI 事件循环集成的低级细节。有关 Matplotlib 事件 API 的更实用介绍,请参阅事件处理系统交互式教程使用 Matplotlib 的交互式应用程序

事件循环#

从根本上说,所有用户交互(和网络)都被实现为一个无限循环,等待来自用户的事件(通过操作系统),然后对其进行处理。例如,最小的读取评估打印循环 (REPL) 是

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

这缺少许多细节(例如,它在第一个异常时退出!),但它代表了所有终端、GUI 和服务器[ 1 ]基础的事件循环。一般来说,Read步骤正在等待某种 I/O——无论是用户输入还是网络——而EvaluatePrint负责解释输入,然后对其进行处理。

在实践中,我们与一个框架进行交互,该框架提供了一种机制来注册回调以响应特定事件而运行,而不是直接实现 I/O 循环[ 2 ]。例如“当用户点击此按钮时,请运行此函数”或“当用户点击'z'键时,请运行此其他函数”。这允许用户编写反应式、事件驱动的程序,而无需深入研究 I/O 的本质[ 3 ]细节。核心事件循环有时被称为“主循环”,根据库的不同,通常由名称为_execrun或的方法启动start

所有 GUI 框架(Qt、Wx、Gtk、tk、OSX 或 web)都有一些方法来捕获用户交互并将它们传递回应用程序(例如QtSignalSlot的框架),但具体细节取决于工具包。Matplotlib 为我们支持的每个 GUI 工具包都有一个后端,它使用工具包 API 将工具包 UI 事件桥接到 Matplotlib 的事件处理系统。然后,您可以使用 FigureCanvasBase.mpl_connect将您的函数连接到 Matplotlib 的事件处理系统。这允许您直接与数据交互并编写与 GUI 工具包无关的用户界面。

命令提示符集成#

到目前为止,一切都很好。我们有 REPL(如 IPython 终端),它允许我们以交互方式将代码发送到解释器并返回结果。我们还有 GUI 工具包,它运行一个事件循环等待用户输入,并让我们注册函数以在发生这种情况时运行。但是,如果我们想要两者都做,我们会遇到一个问题:提示和 GUI 事件循环都是无限循环,每个人都认为自己 负责!为了让提示和 GUI 窗口都能响应,我们需要一种方法来允许循环“分时”:

  1. 当您需要交互式窗口时,让 GUI 主循环阻止 python 进程

  2. 让 CLI 主循环阻塞 python 进程并间歇性地运行 GUI 循环

  3. 在 GUI 中完全嵌入 python(但这基本上是在编写一个完整的应用程序)

阻止提示#

pyplot.show

显示所有打开的数字。

pyplot.pause

为间隔秒运行 GUI 事件循环。

backend_bases.FigureCanvasBase.start_event_loop

启动阻塞事件循环。

backend_bases.FigureCanvasBase.stop_event_loop

停止当前的阻塞事件循环。

最简单的“集成”是以“阻塞”模式启动 GUI 事件循环并接管 CLI。当 GUI 事件循环正在运行时,您不能在提示符中输入新命令(您的终端可能会回显输入到终端中的字符,但它们不会被发送到 Python 解释器,因为它正忙于运行 GUI 事件循环),但是图形窗口将响应。一旦事件循环停止(使任何仍然打开的图形窗口无响应),您将能够再次使用提示。重新启动事件循环将使任何打开的图形再次响应(并将处理任何排队的用户交互)。

要启动事件循环直到所有打开的图形都关闭,请使用 pyplot.showas

pyplot.show(block=True)

要在固定时间(以秒为单位)内启动事件循环,请使用 pyplot.pause.

如果您不使用,您可以通过和 pyplot启动和停止事件循环。但是,在您不会使用的大多数情况下,您将Matplotlib 嵌入到大型 GUI 应用程序中,并且 GUI 事件循环应该已经为应用程序运行。FigureCanvasBase.start_event_loopFigureCanvasBase.stop_event_looppyplot

除了提示之外,如果您想编写一个暂停用户交互的脚本,或者在轮询附加数据之间显示一个图形,这种技术可能非常有用。 有关详细信息,请参阅脚本和函数。

输入钩子集成#

虽然以阻塞模式运行 GUI 事件循环或显式处理 UI 事件很有用,但我们可以做得更好!我们真的希望能够有一个可用的提示交互式图形窗口。

我们可以使用交互式提示的“输入挂钩”功能来做到这一点。这个钩子在等待用户输入时由提示调用(即使对于快速打字员,提示主要是在等待人类思考和移动他们的手指)。尽管提示之间的细节有所不同,但逻辑大致是

  1. 开始等待键盘输入

  2. 启动 GUI 事件循环

  3. 一旦用户按下一个键,退出 GUI 事件循环并处理该键

  4. 重复

这给了我们同时拥有交互式 GUI 窗口和交互式提示的错觉。大多数情况下,GUI 事件循环都在运行,但一旦用户开始输入提示,就会再次接管。

这种分时技术只允许事件循环在 python 空闲并等待用户输入时运行。如果您希望 GUI 在长时间运行的代码期间做出响应,则需要定期刷新 GUI 事件队列,如上所述。在这种情况下,阻塞进程的是您的代码,而不是 REPL,因此您需要手动处理“分时”。相反,一个非常缓慢的图形绘制将阻止提示,直到它完成绘制。

完全嵌入#

也可以走另一个方向,将图形(和Python 解释器)完全嵌入到丰富的本机应用程序中。Matplotlib 为每个工具包提供了可以直接嵌入到 GUI 应用程序中的类(这就是内置窗口的实现方式!)。有关更多详细信息,请参阅在图形用户界面中嵌入 Matplotlib

脚本和函数#

backend_bases.FigureCanvasBase.flush_events

刷新图形的 GUI 事件。

backend_bases.FigureCanvasBase.draw_idle

一旦控制返回到 GUI 事件循环,请求一个小部件重绘。

figure.Figure.ginput

阻止与图形交互的调用。

pyplot.ginput

阻止与图形交互的调用。

pyplot.show

显示所有打开的数字。

pyplot.pause

为间隔秒运行 GUI 事件循环。

在脚本中使用交互式图形有几个用例:

  • 捕获用户输入以控制脚本

  • 随着长时间运行的脚本的进展,进度更新

  • 从数据源流式更新

阻塞函数#

如果您只需要在 Axes 中收集点,您可以使用 Figure.ginput或更一般地工具中 blocking_input的工具将负责为您启动和停止事件循环。但是,如果您编写了一些自定义事件处理或正在使用widgets,则需要使用上述方法手动运行 GUI 事件循环

您还可以使用阻止提示 以挂起运行 GUI 事件循环中描述的方法。一旦循环退出,您的代码将恢复。time.sleep通常,您可以使用 任何您想使用的地方来pyplot.pause代替交互式图形的额外好处。

例如,如果你想轮询数据,你可以使用类似的东西

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

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

它将轮询新数据并以 1Hz 更新数字。

显式旋转事件循环#

backend_bases.FigureCanvasBase.flush_events

刷新图形的 GUI 事件。

backend_bases.FigureCanvasBase.draw_idle

一旦控制返回到 GUI 事件循环,请求一个小部件重绘。

如果您打开的窗口有待处理的 UI 事件(鼠标单击、按钮按下或绘制),您可以通过调用显式处理这些事件FigureCanvasBase.flush_events。这将运行 GUI 事件循环,直到处理完当前等待的所有 UI 事件。确切的行为取决于后端,但通常会处理所有图形上的事件,并且只会处理等待处理的事件(不是在处理期间添加的事件)。

例如

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)

虽然这会感觉有点迟钝(因为我们只每 100 毫秒处理一次用户输入,而 20-30 毫秒是感觉“响应”的时间)它会响应。

如果您对绘图进行更改并希望重新渲染它,您将需要调用draw_idle以请求重新绘制画布。这种方法可以被认为类似于 draw_soonasyncio.loop.call_soon

我们可以将其添加到上面的示例中

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)

您调用得越频繁,您FigureCanvasBase.flush_events的图形就会感觉越灵敏,但代价是在可视化上花费更多资源而在计算上花费更少。

陈旧的艺术家#

艺术家(从 Matplotlib 1.5 开始)有一个stale属性,即 True自上次渲染以来艺术家的内部状态是否发生了变化。默认情况下,陈旧状态会传播到绘图树中的 Artists 父级,例如,如果Line2D 实例的颜色发生更改,则包含它的Axesand也将被标记为“陈旧”。Figure因此,fig.stale将报告图中是否有任何艺术家已被修改并且与屏幕上显示的内容不同步。这旨在用于确定是否draw_idle应该调用来安排图形的重新渲染。

每个艺术家都有一个Artist.stale_callback属性,该属性包含带有签名的回调

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

默认情况下,它设置为将陈旧状态转发给艺术家父级的函数。如果您希望禁止给定艺术家传播,请将此属性设置为无。

Figure实例没有包含艺术家,它们的默认回调是None. 如果您打电话pyplot.ion但不在, IPython我们将安装一个回调,以便 在陈旧draw_idle时 调用。Figure在执行用户输入之后,但在将提示返回给用户之前,IPython我们使用 'post_execute'钩子调用 draw_idle任何陈旧的数字。如果您不使用pyplot,您可以使用回调 Figure.stale_callback属性在图形变得陈旧时收到通知。

空闲抽奖#

backend_bases.FigureCanvasBase.draw

渲染Figure.

backend_bases.FigureCanvasBase.draw_idle

一旦控制返回到 GUI 事件循环,请求一个小部件重绘。

backend_bases.FigureCanvasBase.flush_events

刷新图形的 GUI 事件。

在几乎所有情况下,我们建议使用 backend_bases.FigureCanvasBase.draw_idleover backend_bases.FigureCanvasBase.drawdraw强制渲染图形,而draw_idle在下次 GUI 窗口重新绘制屏幕时安排渲染。这通过仅渲染将在屏幕上显示的像素来提高性能。如果您想确保屏幕尽快更新,请执行

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

线程#

大多数 GUI 框架要求对屏幕的所有更新,以及它们的主事件循环,都在主线程上运行。这使得将绘图的定期更新推送到后台线程是不可能的。虽然看起来倒退,但将计算推送到后台线程并定期更新主线程上的图形通常更容易。

一般来说 Matplotlib 不是线程安全的。如果您要 Artist在一个线程中更新对象并从另一个线程中绘制,您应该确保您锁定在临界区。

Eventloop 集成机制#

CPython / readline #

Python C API 提供了一个钩子 ,PyOS_InputHook来注册要运行的函数(“当 Python 的解释器提示即将空闲并等待来自终端的用户输入时,将调用该函数。”)。此钩子可用于将第二个事件循环(GUI 事件循环)与 python 输入提示循环集成。钩子函数通常会耗尽 GUI 事件队列上的所有未决事件,运行主循环一段固定的短时间,或者运行事件循环直到在标准输入上按下一个键。

PyOS_InputHook由于 Matplotlib 的使用方式广泛,Matplotlib目前不做任何管理。这种管理留给下游库——用户代码或外壳。PyOS_InputHook交互式图形,即使 Matplotlib 处于“交互模式”,如果未注册适当的,可能无法在 vanilla python repl 中工作。

输入挂钩和安装它们的帮助程序通常包含在 GUI 工具包的 python 绑定中,并且可以在导入时注册。IPython 还为 Matplotlib 支持的所有 GUI 框架提供了输入钩子函数,这些框架可以通过%matplotlib. 这是集成 Matplotlib 和提示的推荐方法。

IPython / prompt_toolkit #

随着 IPython >= 5.0,IPython 已经从使用 CPython 的基于 readline 的提示变为prompt_toolkit基于提示。prompt_toolkit 具有相同的概念输入钩子,prompt_toolkit通过该 IPython.terminal.interactiveshell.TerminalInteractiveShell.inputhook() 方法输入。prompt_toolkit输入挂钩 的来源位于IPython.terminal.pt_inputhooks.

脚注