matplotlib 是一个基于 Python 的绘图库,它全面支持二维图形,并对三维图形提供有限支持,在 Python 科学计算社区中被广泛使用。该库针对各种用例。它可以将图形嵌入您选择的用户界面工具包中,目前支持使用 GTK+、Qt、Tk、FLTK、wxWidgets 和 Cocoa 工具包在所有主要桌面操作系统上进行交互式图形。它可以从交互式 Python shell 交互式调用,以使用简单的过程命令生成图形,非常类似于 Mathematica、IDL 或 MATLAB。matplotlib 还可以嵌入到无头的 Web 服务器中,以提供基于光栅的格式(如便携式网络图形 (PNG))和矢量格式(如 PostScript、便携式文档格式 (PDF) 和可缩放矢量图形 (SVG))的硬拷贝,这些格式在纸上看起来很棒。
matplotlib 的起源可以追溯到我们中的一个人(John Hunter)试图让自己和他的癫痫研究同事从一个专有软件包中解放出来,该软件包用于进行皮层脑电图 (ECoG) 分析。他工作的实验室只有一份该软件的许可证,各种研究生、医学生、博士后、实习生和研究人员轮流共享硬件密钥。MATLAB 在生物医学领域被广泛用于数据分析和可视化,因此 Hunter 开始着手,并取得了一定成功,用基于 MATLAB 的版本替换专有软件,该版本可以被多个研究人员利用和扩展。然而,MATLAB 自然地将世界视为一系列浮点数,而现实世界中癫痫手术患者的医院记录的复杂性(多种数据模式(CT、MRI、ECoG、EEG))存储在不同的服务器上,将 MATLAB 作为数据管理系统的局限性推到了极限。由于对 MATLAB 的这种任务的适用性不满意,Hunter 开始开发一个新的 Python 应用程序,该应用程序构建在用户界面工具包 GTK+ 之上,该工具包当时是 Linux 的领先桌面窗口系统。
因此,matplotlib 最初被开发为该 GTK+ 应用程序的 EEG/ECoG 可视化工具,并且这种用例指导了其最初的架构。matplotlib 最初被设计用来满足第二个目的:作为交互式命令驱动图形生成的替代品,这是 MATLAB 做得非常好的事情。MATLAB 设计使简单的加载数据文件和绘图任务变得非常简单,而全面的面向对象 API 将在语法上过于繁重。因此,matplotlib 还提供了一个有状态脚本接口,用于快速轻松地生成类似于 MATLAB 的图形。因为 matplotlib 是一个库,所以用户可以访问所有丰富的内置 Python 数据结构,例如列表、字典、集合等等。
包含和管理给定图形中所有元素的顶层 matplotlib 对象称为Figure
。matplotlib 必须解决的核心架构任务之一是实现一个框架来表示和操作Figure
,该框架与将Figure
渲染到用户界面窗口或硬拷贝的行为分离。这使我们能够在Figure
中构建越来越复杂的功能和逻辑,同时保持“后端”或输出设备相对简单。matplotlib 不仅封装了绘图接口以允许渲染到多个设备,还封装了大多数流行的用户界面工具包的基本事件处理和窗口处理。因此,用户可以创建相当丰富的交互式图形和工具包,这些工具包包含鼠标和键盘输入,并且可以无需修改地插入我们支持的六个用户界面工具包中。
实现此目的的架构在逻辑上分为三层,可以看作是一个堆栈。每个位于另一层之上的层都知道如何与它下面的层进行通信,但较低的层不知道它上面的层。从下到上的三层分别是:后端、艺术家和脚本。
堆栈的底部是后端层,它提供抽象接口类的具体实现
FigureCanvas
封装了绘制表面的概念(例如“纸张”)。Renderer
进行绘制(例如“画笔”)。Event
处理用户输入,例如键盘和鼠标事件。对于诸如 Qt 之类的用户界面工具包,FigureCanvas
有一个具体实现,它知道如何将自己插入到本机 Qt 窗口 (QtGui.QMainWindow
) 中,将 matplotlib Renderer 命令传输到画布 (QtGui.QPainter
),并将本机 Qt 事件转换为 matplotlib Event
框架,该框架向回调调度程序发出信号以生成事件,以便上游侦听器可以处理它们。抽象基类位于matplotlib.backend_bases
中,所有派生类都位于专用模块中,例如matplotlib.backends.backend_qt4agg
。对于专门用于生成硬拷贝输出(如 PDF、PNG、SVG 或 PS)的纯图像后端,FigureCanvas
实现可能会简单地设置一个文件类对象,在其中定义默认标头、字体和宏函数,以及Renderer
创建的各个对象(线、文本、矩形等)。
Renderer
的工作是为将墨水放置在画布上提供低级绘图接口。如上所述,最初的 matplotlib 应用程序是 GTK+ 应用程序中的 ECoG 查看器,并且大部分原始设计灵感来自当时可用的 GDK/GTK+ API。最初的Renderer
API 受 GDK Drawable
接口的驱动,该接口实现了诸如draw_point
、draw_line
、draw_rectangle
、draw_image
、draw_polygon
和draw_glyphs
等基本方法。我们实现的每个附加后端(最早的是 PostScript 后端和 GD 后端)都实现了 GDK Drawable
API 并将这些 API 转换为本机后端相关的绘图命令。正如我们在下面讨论的那样,这在不必要地使新后端实现复杂化,导致大量方法激增,并且此 API 后来被大幅简化,从而使将 matplotlib 移植到新的用户界面工具包或文件规范成为一个简单的过程。
matplotlib 的设计决策之一是支持使用 C++ 模板库 Anti-Grain Geometry 或“agg”[She06] 的核心基于像素的渲染器。这是一个用于渲染抗锯齿二维图形的高性能库,它生成具有吸引力的图像。matplotlib 支持将 agg 后端渲染的像素缓冲区插入我们支持的每个用户界面工具包,因此您可以在 UI 和操作系统之间获得像素精确的图形。因为 matplotlib 生成的 PNG 输出也使用 agg 渲染器,所以硬拷贝与屏幕显示相同,因此在 UI、操作系统和 PNG 输出之间,您看到的就是您得到的。
matplotlib Event
框架将底层 UI 事件(如key-press-event
或mouse-motion-event
)映射到 matplotlib 类KeyEvent
或MouseEvent
。用户可以连接到这些事件以回调函数并与他们的图形和数据交互;例如,pick
一个数据点或一组点,或操作图形或其组成部分的某些方面。以下代码示例说明了如何在用户键入 `t` 时切换Axes
窗口中的所有线。
import numpy as np import matplotlib.pyplot as plt def on_press(event): if event.inaxes is None: return for line in event.inaxes.lines: if event.key=='t': visible = line.get_visible() line.set_visible(not visible) event.inaxes.figure.canvas.draw() fig, ax = plt.subplots(1) fig.canvas.mpl_connect('key_press_event', on_press) ax.plot(np.random.rand(2, 20)) plt.show()
对底层 UI 工具包事件框架的抽象使 matplotlib 开发人员和最终用户能够以“一次编写,随处运行”的方式编写 UI 事件处理代码。例如,在所有用户界面工具包中都能工作的 matplotlib 图形的交互式平移和缩放是在 matplotlib 事件框架中实现的。
Artist
层次结构是 matplotlib 堆栈的中间层,也是大部分繁重工作发生的地方。继续使用来自后端的FigureCanvas
是纸张的类比,Artist
是知道如何使用Renderer
(画笔)将墨水放在画布上的对象。您在 matplotlib Figure
中看到的一切都是一个Artist
实例;标题、线、刻度标签、图像等等都对应于单独的Artist
实例(参见图 11.3)。基类是matplotlib.artist.Artist
,它包含每个Artist
共享的属性:将艺术家坐标系转换为画布坐标系的变换(将在下面详细讨论)、可见性、定义艺术家可以绘制到的区域的剪辑框、标签以及处理用户交互的接口,例如“拾取”;也就是说,检测鼠标单击何时发生在艺术家身上。
Artist
层次结构与后端之间的耦合发生在draw
方法中。例如,在下面我们创建子类化Artist
的SomeArtist
的模拟类中,SomeArtist
必须实现的基本方法是draw
,该方法传递来自后端的渲染器。Artist
不知道渲染器将要绘制到的后端类型(PDF、SVG、GTK+ DrawingArea 等),但它确实知道Renderer
API 并将调用适当的方法(draw_text
或draw_path
)。由于Renderer
具有指向其画布的指针并知道如何在其上绘制,因此draw
方法将Artist
的抽象表示转换为像素缓冲区中的颜色、SVG 文件中的路径或任何其他具体表示。
class SomeArtist(Artist): 'An example Artist that implements the draw method' def draw(self, renderer): """Call the appropriate renderer methods to paint self onto canvas""" if not self.get_visible(): return # create some objects and use renderer to draw self here renderer.draw_path(graphics_context, path, transform)
层次结构中有两种类型的Artist
。基本艺术家表示您在绘图中看到的对象类型:Line2D
、Rectangle
、Circle
和Text
。复合艺术家是Artist
的集合,例如Axis
、Tick
、Axes
和Figure
。每个复合艺术家可能包含其他复合艺术家以及基本艺术家。例如,Figure
包含一个或多个复合Axes
,而Figure
的背景是一个基本Rectangle
。
最重要的复合艺术家是Axes
,大多数 matplotlib API 绘图方法都定义在其中。Axes
不仅包含构成绘图背景的大多数图形元素(刻度线、轴线、网格、绘图背景的颜色块),还包含许多帮助方法,这些方法创建基本艺术家并将它们添加到Axes
实例中。例如,表 11.1 显示了创建绘图对象并将它们存储在Axes
实例中的少量Axes
方法样本。
方法 | 创建 | 存储在 |
Axes.imshow | 一个或多个matplotlib.image.AxesImage | Axes.images |
Axes.hist | 许多matplotlib.patch.Rectangle | Axes.patches |
Axes.plot | 一个或多个matplotlib.lines.Line2D | Axes.lines |
下面是一个简单的 Python 脚本,演示了上面的架构。它定义了后端,将一个 Figure
连接到它,使用数组库 numpy
创建 10,000 个正态分布的随机数,并绘制这些随机数的直方图。
# Import the FigureCanvas from the backend of your choice # and attach the Figure artist to it. from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas from matplotlib.figure import Figure fig = Figure() canvas = FigureCanvas(fig) # Import the numpy library to generate the random numbers. import numpy as np x = np.random.randn(10000) # Now use a figure method to create an Axes artist; the Axes artist is # added automatically to the figure container fig.axes. # Here "111" is from the MATLAB convention: create a grid with 1 row and 1 # column, and use the first cell in that grid for the location of the new # Axes. ax = fig.add_subplot(111) # Call the Axes method hist to generate the histogram; hist creates a # sequence of Rectangle artists for each histogram bar and adds them # to the Axes container. Here "100" means create 100 bins. ax.hist(x, 100) # Decorate the figure with a title and save it. ax.set_title('Normal distribution with $\mu=0, \sigma=1$') fig.savefig('matplotlib_histogram.png')
使用上面 API 的脚本运行良好,尤其适合程序员,并且通常是在编写 Web 应用程序服务器、UI 应用程序,或者可能是要与其他开发人员共享的脚本时合适的编程范例。对于日常目的,特别是对于非专业程序员的基准科学家进行的交互式探索性工作,它在语法上有点繁重。大多数用于数据分析和可视化的专用语言都提供更轻量的脚本接口来简化常见任务,而 matplotlib 在其 matplotlib.pyplot
接口中也这样做。上面相同的代码,使用 pyplot
,读取
import matplotlib.pyplot as plt import numpy as np x = np.random.randn(10000) plt.hist(x, 100) plt.title(r'Normal distribution with $\mu=0, \sigma=1$') plt.savefig('matplotlib_histogram.png') plt.show()
pyplot
是一个有状态的接口,它处理创建图形和坐标轴以及将它们连接到您选择的后端的许多样板,并维护表示当前图形和坐标轴的模块级内部数据结构,以便将绘图命令定向到这些结构。
让我们剖析脚本中的重要行,看看如何管理这种内部状态。
import matplotlib.pyplot as plt
:当 pyplot
模块加载时,它会解析一个本地配置文件,用户在其中声明了他们对默认后端的偏好,以及其他许多内容。这可能是一个用户界面后端,例如 QtAgg
,在这种情况下,上面的脚本将导入 GUI 框架并启动一个带有嵌入式绘图的 Qt 窗口,或者它可能是一个纯图像后端,例如 Agg
,在这种情况下,脚本将生成硬拷贝输出并退出。plt.hist(x, 100)
:这是脚本中的第一个绘图命令。pyplot
将检查其内部数据结构,以查看是否存在当前 Figure
实例。如果是,它将提取当前 Axes
并将绘图定向到 Axes.hist
API 调用。在这种情况下,不存在,因此它将创建一个 Figure
和 Axes
,将这些设置为当前,并将绘图定向到 Axes.hist
。plt.title(r'Normal distribution with $\mu=0, \sigma=1$')
:如上所述,pyplot 将查看是否存在当前 Figure
和 Axes
。发现存在后,它不会创建新实例,而是将调用定向到现有 Axes
实例方法 Axes.set_title
。plt.show()
:这将强制 Figure
渲染,如果用户在其配置文件中指定了默认的 GUI 后端,则将启动 GUI 主循环并将创建的所有图形提升到屏幕上。下面显示了 pyplot
的常用线绘图函数 matplotlib.pyplot.plot
的一个简化版本,用于说明 pyplot 函数如何包装 matplotlib 面向对象的内核中的功能。所有其他 pyplot
脚本接口函数都遵循相同的设计。
@autogen_docstring(Axes.plot) def plot(*args, **kwargs): ax = gca() ret = ax.plot(*args, **kwargs) draw_if_interactive() return ret
Python 装饰器 @autogen_docstring(Axes.plot)
从相应的 API 方法中提取文档字符串,并将格式正确的版本附加到 pyplot.plot
方法;我们有一个专门的模块 matplotlib.docstring
来处理这个文档字符串魔法。文档签名中的 *args
和 **kwargs
是 Python 中的特殊约定,表示传递给该方法的所有参数和关键字参数。这使我们能够将它们转发到相应的 API 方法。调用 ax = gca()
调用有状态的机制来“获取当前 Axes”(每个 Python 解释器只能有一个“当前 Axes”),并且将在必要时创建 Figure
和 Axes
。对 ret = ax.plot(*args, **kwargs)
的调用将函数调用及其参数转发到相应的 Axes
方法,并将返回值存储起来以便稍后返回。因此,pyplot
接口是核心 Artist
API 的一个相当薄的包装器,它试图通过在脚本接口中公开 API 函数、调用签名和文档字符串,并使用最少量的样板代码,来避免尽可能多的代码重复。
随着时间的推移,输出后端的绘图 API 增长了大量方法,包括
draw_arc, draw_image, draw_line_collection, draw_line, draw_lines, draw_point, draw_quad_mesh, draw_polygon_collection, draw_polygon, draw_rectangle, draw_regpoly_collection
不幸的是,拥有更多后端方法意味着编写新后端需要更长时间,并且随着核心添加新功能,更新现有后端需要大量工作。由于每个后端都是由精通特定输出文件格式的单个开发人员实现的,因此新功能有时需要很长时间才能出现在所有后端中,这会导致用户对哪些功能在哪些地方可用感到困惑。
对于 matplotlib 版本 0.98,后端被重构,只要求后端本身具有最基本的功能,其他所有内容都移到了核心。后端 API 中所需的方
draw_path
:绘制由线段和 Béezier 段组成的复合多边形。此接口取代了许多旧方法:draw_arc
、draw_line
、draw_lines
和 draw_rectangle
。draw_image
:绘制光栅图像。draw_text
:使用给定的字体属性绘制文本。get_text_width_height_descent
:给定一个文本字符串,返回其度量。可以使用这些方法实现新后端所需的所有绘图。(我们还可以更进一步,使用 draw_path
绘制文本,从而不再需要 draw_text
方法,但我们还没有完成这项简化工作。当然,后端仍然可以自由地实现自己的 draw_text
方法来输出“真实”文本。)这对于更容易地启动和运行新后端非常有用。但是,在某些情况下,后端可能希望覆盖核心的行为,以创建更有效的输出。例如,在绘制标记(用于指示线图中顶点的较小符号)时,将标记的形状只写入文件一次,然后将其作为“图章”重复使用,这样在空间效率上更高。在这种情况下,后端可以实现 draw_markers
方法。如果它被实现,后端会将标记形状写入一次,然后写入更短的命令来在多个位置重复使用它。如果它没有被实现,核心将简单地使用对 draw_path
的多次调用来多次绘制标记。
可选后端 API 方法的完整列表是
draw_markers
:绘制一组标记。draw_path_collection
:绘制路径集合。draw_quad_mesh
:绘制四边形网格。matplotlib 花费大量时间将坐标从一个系统转换为另一个系统。这些坐标系包括
每个 Artist
都有一个转换节点,它知道如何从一个坐标系转换为另一个坐标系。这些转换节点在有向图中连接在一起,其中每个节点都依赖于其父节点。通过沿着边缘一直到图的根,可以将数据空间中的坐标一直转换为最终输出文件中的坐标。大多数转换也是可逆的。这使得可以单击绘图中的元素并返回其数据空间中的坐标。转换图设置了转换节点之间的依赖关系:当父节点的转换发生变化时,例如当 Axes
的限制发生变化时,与该 Axes
相关的任何转换都会失效,因为它们需要重新绘制。当然,与图形中其他 Axes
相关的转换可能会保留,从而防止不必要的重新计算,并有助于提高交互式性能。
转换节点可以是简单的仿射变换,也可以是非仿射变换。仿射变换是保留直线和距离比例的变换族,包括旋转、平移、缩放和倾斜。二维仿射变换使用 3×3 仿射变换矩阵表示。变换后的点 (x', y') 是通过将原始点 (x, y) 与该矩阵相乘得到的
然后,二维坐标可以通过简单地将它们与变换矩阵相乘来轻松地进行变换。仿射变换还具有一个有用的属性,即它们可以使用矩阵乘法进行组合。这意味着,要执行一系列仿射变换,可以先将变换矩阵乘在一起,然后将所得矩阵用于变换坐标。matplotlib 的转换框架在变换坐标之前会自动组合(冻结)仿射变换矩阵,以减少计算量。拥有快速的仿射变换很重要,因为它使 GUI 窗口中的交互式平移和缩放更有效率。
matplotlib 中的非仿射变换使用 Python 函数定义,因此它们是真正任意的。在 matplotlib 核心内,非仿射变换用于对数刻度、极坐标图和地理投影 (图 11.5)。这些非仿射变换可以自由地与转换图中的仿射变换混合使用。matplotlib 将自动简化仿射部分,并且只在非仿射部分回退到任意函数。
从这些简单的部分,matplotlib 可以做一些非常高级的事情。混合变换是一个特殊的变换节点,它对 x 轴使用一个变换,对 y 轴使用另一个变换。这当然只有在给定的变换是“可分离的”时才有可能,这意味着 x 和 y 坐标是独立的,但变换本身可以是仿射的,也可以是非仿射的。例如,这用于绘制对数图,其中 x 和 y 轴中的一个或两个都可能有对数刻度。拥有混合变换节点允许以任意方式组合可用的刻度。转换图允许的另一件事是坐标轴共享。可以将一个绘图的限制“链接”到另一个绘图,并确保当一个绘图进行平移或缩放时,另一个绘图会相应地更新。在这种情况下,同一个转换节点只是在两个坐标轴之间共享,这两个坐标轴甚至可能位于两个不同的图形上。 图 11.6 显示了一个具有这些高级功能的转换图示例。axes1 有一个对数 x 轴;axes1 和 axes2 共享同一个 y 轴。
在绘制线图时,需要执行许多步骤才能从原始数据到屏幕上绘制的线条。在 matplotlib 的早期版本中,所有这些步骤都是交织在一起的。它们已被重构,因此它们是“路径转换”管道中的离散步骤。这使每个后端都可以选择执行管道的哪些部分,因为有些部分只在特定情况下有用。
NaN
,或使用numpy
掩码数组来指示这一点。矢量输出格式(如PDF)和渲染库(如Agg)在绘制折线时通常没有缺失数据的概念,因此管道中的这一步必须使用MOVETO
命令跳过缺失的数据段,这些命令告诉渲染器提起笔,并在新的点开始重新绘制。由于 matplotlib 的用户通常是科学家,因此在绘图上直接放置格式丰富的数学表达式很有用。也许最广泛使用的数学表达式语法来自唐纳德·克努特的 TeX 排版系统。它是一种将像这样的纯文本语言中的输入转换为格式正确的数学表达式的方法。
\sqrt{\frac{\delta x}{\delta y}}
转换为格式正确的数学表达式。
matplotlib 提供两种渲染数学表达式的方法。第一种方法是usetex
,它使用用户机器上的完整 TeX 副本来渲染数学表达式。TeX 以其本机 DVI(设备无关)格式输出表达式中字符和行的位置。matplotlib 然后解析 DVI 文件并将其转换为一组绘图命令,然后 matplotlib 的一个输出后端直接在绘图上渲染这些命令。这种方法可以处理大量晦涩的数学语法。但是,它要求用户完全安装并运行 TeX。因此,matplotlib 还包含自己的内部数学渲染引擎,称为mathtext
。
mathtext
是 TeX 数学渲染引擎的直接移植,粘贴到一个更简单的解析器上,该解析器使用pyparsing
[McG07] 解析框架编写。此移植基于已发布的 TeX 源代码副本 [Knu86]。简单的解析器构建一个由方框和胶水(用 TeX 术语来说)组成的树,然后由布局引擎进行布局。虽然包含了完整的 TeX 数学渲染引擎,但没有包含大量第三方 TeX 和 LaTeX 数学库。此类库中的功能将根据需要移植,并首先重点关注经常使用且非学科特定的功能。这为渲染大多数数学表达式提供了一种不错且轻量级的方式。
历史上,matplotlib 并没有大量低级单元测试。偶尔,如果出现严重错误,则会将一个用于重现该错误的脚本添加到源树中此类文件的目录中。缺少自动化测试会导致所有常见的问题,最重要的是,以前有效的特性的回归。(我们可能不需要向您说明自动化测试是一件好事。)当然,对于如此多的代码以及如此多的配置选项和可互换组件(例如,后端),可以说,仅靠低级单元测试永远不会足够;相反,我们遵循的信念是,测试所有组件协同工作是最划算的。
为此,作为第一步,编写了一个脚本,该脚本生成了许多用于测试 matplotlib 各种特性的绘图,尤其是那些难以实现正确的特性。这使检测新更改导致意外中断变得更容易,但图像的正确性仍然需要手动验证。由于这需要大量的人工工作,因此很少这样做。
作为第二步,这种通用方法已实现自动化。当前的 matplotlib 测试脚本生成许多绘图,但与需要人工干预不同的是,这些绘图会自动与基线图像进行比较。所有测试都在 nose 测试框架中运行,这使得生成哪些测试失败的报告变得非常容易。
使事情复杂化的是,图像比较不能完全精确。Freetype 字体渲染库版本的细微变化会导致文本的输出在不同机器上略有不同。这些差异不足以被认为是“错误”,但足以扰乱任何精确的逐位比较。相反,测试框架计算两个图像的直方图,并计算其差异的均方根。如果该差异大于给定的阈值,则认为图像差异太大,比较测试失败。当测试失败时,会生成差异图像,显示绘图中发生更改的位置(参见图 11.9)。然后,开发人员可以决定失败是由于有意更改导致的,并更新基线图像以匹配新图像,或者决定图像实际上不正确,并找出并修复导致更改的错误。
由于不同的后端会导致不同的错误,因此测试框架针对每个绘图测试多个后端:PNG、PDF 和 SVG。对于矢量格式,我们不会直接比较矢量信息,因为有多种方法可以表示在栅格化时具有相同最终结果的东西。矢量后端应该能够自由更改其输出的细节,以提高效率,而不会导致所有测试失败。因此,对于矢量后端,测试框架首先使用外部工具(PDF 的 Ghostscript 和 SVG 的 Inkscape)将文件渲染到栅格,然后使用这些栅格进行比较。
使用这种方法,我们能够从头开始更容易地构建一个相当有效的测试框架,而不是继续编写许多低级单元测试。不过,它并不完美;测试的代码覆盖率并不完整,运行所有测试需要很长时间。(在 2.33 GHz 英特尔酷睿 2 双核 E6550 上大约需要 15 分钟。)因此,一些回归仍然会漏网,但总的来说,自测试框架实施以来,版本的质量已大大提高。
从 matplotlib 的开发中学到的重要经验之一是,正如勒·柯布西耶所说,“好的建筑师借鉴”。matplotlib 的早期作者主要是科学家,他们是自学成才的程序员,试图完成自己的工作,而不是经过正规培训的计算机科学家。因此,我们在第一次尝试时没有设计好内部设计。决定实现一个与 MATLAB API 大致兼容的用户界面脚本层,使项目受益匪浅:它提供了一个经过时间考验的接口来创建和自定义图形,它为从庞大的 MATLAB 用户群过渡到 matplotlib 提供了简便的途径,而且——对我们来说,在 matplotlib 架构的背景下,这一点至关重要——它使开发人员能够在不影响大多数用户的情况下多次重构内部面向对象的 API,因为脚本接口保持不变。虽然我们从一开始就拥有 API 用户(而不是脚本用户),但他们中的大多数是能够适应 API 更改的高级用户或开发人员。另一方面,脚本用户可以编写一次代码,并且可以几乎肯定地认为它在所有后续版本中都是稳定的。
对于内部绘图 API,虽然我们确实借鉴了 GDK,但我们没有花足够的时间来确定这是否是最合适的绘图 API,并且在围绕此 API 编写了许多后端之后,不得不付出相当大的努力来扩展功能,以围绕更简单且更灵活的绘图 API 扩展功能。如果我们采用 PDF 绘图规范 [Ent11b],我们将会得到很好的回报,该规范本身是基于 Adobe 在其 PostScript 规范方面的数十年经验而开发的;它将使我们能够在很大程度上直接与 PDF 本身、Quartz Core Graphics 框架和 Enthought Enable Kiva 绘图工具包 [Ent11a] 兼容。
Python 的弊端之一是它是一种非常简单且富有表现力的语言,以至于开发人员通常会发现,与其努力集成来自其他包的代码,不如重新发明和重新实现其他包中已有的功能更容易。matplotlib 在早期开发中本可以从花费更多的时间来集成现有模块和 API(例如 Enthought 的 Kiva 和 Enable 工具包)中获益,这些工具包解决了许多类似的问题,而不是重新发明功能。然而,与现有功能的集成是一把双刃剑,因为它会使构建和发布变得更加复杂,并降低内部开发的灵活性。