Matplotlib 应用程序接口 (APIs)#
Matplotlib 有两个主要的应用接口,或者说使用该库的两种风格:
一个显式的“Axes”接口,它使用Figure或Axes对象上的方法来创建其他Artists,并逐步构建可视化。这也被称为“面向对象”接口。
一个隐式的“pyplot”接口,它跟踪最后创建的图形和轴,并将艺术家添加到用户想要的对象中。
此外,许多下游库(如 pandas 和 xarray)在其数据类上直接实现了 plot 方法,以便用户可以调用 data.plot()。
这些接口之间的区别可能会让人有些困惑,特别是考虑到网络上的一些片段使用了其中一种或另一种,有时甚至在同一个例子中使用了多种接口。在这里,我们试图指出“pyplot”和下游接口与显式的“Axes”接口之间的关系,以帮助用户更好地导航这个库。
原生 Matplotlib 接口#
显式的“Axes”接口#
“Axes” 接口是 Matplotlib 的实现方式,许多自定义和微调最终都在这个层面上进行。
此接口通过实例化一个 Figure 类(下文称为 fig),使用该对象上的 subplots 方法(或类似方法)创建一个或多个 Axes 对象(下文称为 ax),然后在 Axes 上调用绘图方法(本例中为 plot)来工作:
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.subplots()
ax.plot([1, 2, 3, 4], [0, 0.5, 1, 0.2])
(Source code, 2x.png, png)
我们称这种为“显式”接口,因为每个对象都被显式引用,并用于创建下一个对象。保留对对象的引用非常灵活,并允许我们在对象创建后但在显示之前对其进行自定义。
隐式的“pyplot”接口#
pyplot 模块覆盖了大部分 Axes 绘图方法,以提供与上述等效的功能,其中图和轴的创建由用户完成:
import matplotlib.pyplot as plt
plt.plot([1, 2, 3, 4], [0, 0.5, 1, 0.2])
(Source code, 2x.png, png)
这在进行交互式工作或简单脚本时特别方便。可以使用 gcf 获取当前 Figure 的引用,使用 gca 获取当前 Axes 的引用。pyplot 模块保留了一个 Figure 列表,并且每个 Figure 保留了一个图表上 Axes 的列表,以便用户执行以下操作:
import matplotlib.pyplot as plt
plt.subplot(1, 2, 1)
plt.plot([1, 2, 3], [0, 0.5, 0.2])
plt.subplot(1, 2, 2)
plt.plot([3, 2, 1], [0, 0.5, 0.2])
(Source code, 2x.png, png)
等同于:
import matplotlib.pyplot as plt
plt.subplot(1, 2, 1)
ax = plt.gca()
ax.plot([1, 2, 3], [0, 0.5, 0.2])
plt.subplot(1, 2, 2)
ax = plt.gca()
ax.plot([3, 2, 1], [0, 0.5, 0.2])
(Source code, 2x.png, png)
在显式接口中,这将是:
import matplotlib.pyplot as plt
fig, axs = plt.subplots(1, 2)
axs[0].plot([1, 2, 3], [0, 0.5, 0.2])
axs[1].plot([3, 2, 1], [0, 0.5, 0.2])
(Source code, 2x.png, png)
为什么要明确?#
如果你需要回溯,并对一个不被 plt.gca() 引用的旧轴进行操作,会发生什么?一个简单的方法是使用相同的参数再次调用 subplot。然而,这很快就会变得不优雅。你也可以检查 Figure 对象并获取其 Axes 对象的列表,但这可能会产生误导(颜色条也是 Axes!)。最好的解决方案可能是为每个创建的 Axes 保存一个句柄,但如果你这样做,为什么不一开始就创建所有的 Axes 对象呢?
第一种方法是再次调用 plt.subplot:
import matplotlib.pyplot as plt
plt.subplot(1, 2, 1)
plt.plot([1, 2, 3], [0, 0.5, 0.2])
plt.subplot(1, 2, 2)
plt.plot([3, 2, 1], [0, 0.5, 0.2])
plt.suptitle('Implicit Interface: re-call subplot')
for i in range(1, 3):
plt.subplot(1, 2, i)
plt.xlabel('Boo')
(Source code, 2x.png, png)
第二种是保存一个句柄:
import matplotlib.pyplot as plt
axs = []
ax = plt.subplot(1, 2, 1)
axs += [ax]
plt.plot([1, 2, 3], [0, 0.5, 0.2])
ax = plt.subplot(1, 2, 2)
axs += [ax]
plt.plot([3, 2, 1], [0, 0.5, 0.2])
plt.suptitle('Implicit Interface: save handles')
for i in range(2):
plt.sca(axs[i])
plt.xlabel('Boo')
(Source code, 2x.png, png)
然而,推荐的方式是从一开始就明确表达:
import matplotlib.pyplot as plt
fig, axs = plt.subplots(1, 2)
axs[0].plot([1, 2, 3], [0, 0.5, 0.2])
axs[1].plot([3, 2, 1], [0, 0.5, 0.2])
fig.suptitle('Explicit Interface')
for i in range(2):
axs[i].set_xlabel('Boo')
(Source code, 2x.png, png)
第三方库 "数据对象" 接口#
一些第三方库已经选择为他们的数据对象实现绘图功能,例如在 pandas、xarray 和其他第三方库中可以看到 data.plot()。为了说明目的,下游库可能会实现一个简单的数据容器,该容器将 x 和 y 数据存储在一起,然后实现一个 plot 方法:
import matplotlib.pyplot as plt
# supplied by downstream library:
class DataContainer:
def __init__(self, x, y):
"""
Proper docstring here!
"""
self._x = x
self._y = y
def plot(self, ax=None, **kwargs):
if ax is None:
ax = plt.gca()
ax.plot(self._x, self._y, **kwargs)
ax.set_title('Plotted from DataClass!')
return ax
# what the user usually calls:
data = DataContainer([0, 1, 2, 3], [0, 0.2, 0.5, 0.3])
data.plot()
(Source code, 2x.png, png)
因此,该库可以向用户隐藏所有繁琐的细节,并可以根据数据类型生成适当的可视化效果,通常带有良好的标签、颜色映射选择和其他便利功能。
然而,在上面的例子中,我们可能不喜欢库提供的标题。幸运的是,它们通过 plot() 方法返回了 Axes,理解显式的 Axes 接口后,我们可以调用:ax.set_title('我喜欢的标题') 来自定义标题。
许多库还允许它们的 plot 方法接受一个可选的 ax 参数。这使我们能够将可视化放置在我们放置并可能自定义的 Axes 中。
摘要#
总的来说,理解显式的“Axes”接口是有用的,因为它是最灵活的,并且是其他接口的基础。用户通常可以弄清楚如何切换到显式接口并操作底层对象。虽然显式接口在设置时可能会稍微冗长一些,但复杂的图表通常会比尝试使用隐式的“pyplot”接口更简单。
备注
有时人们会感到困惑,因为我们为两种接口都导入了 pyplot。目前,pyplot 模块实现了“pyplot”接口,但它也提供了顶层的 Figure 和 Axes 创建方法,并且最终会启动图形用户界面(如果正在使用的话)。因此,无论选择哪种接口,pyplot 仍然是必需的。
同样地,合作伙伴库提供的声明式接口使用可通过“Axes”接口访问的对象,并且通常将这些对象作为参数接受或从方法中返回。通常,使用显式的“Axes”接口来执行对默认可视化的任何自定义,或将数据解包到NumPy数组并直接传递给Matplotlib,这是非常必要的。
附录:与数据结构交互的“轴”接口#
大多数 Axes 方法允许通过传递一个 data 对象给方法,并以字符串形式指定参数来进行另一种API调用:
import matplotlib.pyplot as plt
data = {'xdat': [0, 1, 2, 3], 'ydat': [0, 0.2, 0.4, 0.1]}
fig, ax = plt.subplots(figsize=(2, 2))
ax.plot('xdat', 'ydat', data=data)
(Source code, 2x.png, png)
附录:“pylab”接口#
还有一个非常不推荐的接口,那就是基本上执行 from matplotlib.pylab import *。这会从 matplotlib.pyplot、numpy、numpy.fft、numpy.linalg 和 numpy.random 导入所有函数,以及一些额外的函数到全局命名空间中。
这种模式在现代Python中被认为是坏习惯,因为它会污染全局命名空间。更严重的是,在 pylab 的情况下,这将覆盖一些内置函数(例如,内置的 sum 将被 numpy.sum 替换),这可能导致意外行为。