1.2 Figure 和 Axes 对象 本节摘要:Figure 是 Matplotlib 的顶层画布,一张图上所有的内容都挂在它里面;Axes 是画布上的坐标系区域,是真正把数据画出来的地方;而 Artist 是所有可见元素(线、文本、刻度、图例)的共同基类。理解这三者的层级关系,是驾驭 Matplotlib 的第一道门。本节先把"为什么要分画布和坐标系两层"的直觉讲清楚,再逐个拆开这两个对象,最后对比 pyplot 快捷接口和面向对象接口的差异,帮你彻底摆脱"plt 和 ax 到底用哪个"的困惑。
本节摘要:Figure 是 Matplotlib 的顶层画布,一张图上所有的内容都挂在它里面;Axes 是画布上的坐标系区域,是真正把数据画出来的地方;而 Artist 是所有可见元素(线、文本、刻度、图例)的共同基类。理解这三者的层级关系,是驾驭 Matplotlib 的第一道门。本节先把"为什么要分画布和坐标系两层"的直觉讲清楚,再逐个拆开这两个对象,最后对比 pyplot 快捷接口和面向对象接口的差异,帮你彻底摆脱"plt 和 ax 到底用哪个"的困惑。
阅读完本节,你应当能够:
第一次看 Matplotlib 代码的人,几乎都会被 fig, ax = plt.subplots() 这句弄糊涂:为什么画个图要返回两个东西?fig 和 ax 到底谁是图?
答案藏在一个很朴素的现实里:一张纸可以同时画好几幅互不相干的小图,但每幅小图又各有各的横轴纵轴。 如果 Matplotlib 只给你一个对象,你既没法描述"这张大纸多大",也没法描述"这幅小图在哪、它的轴范围是多少"。所以它干脆把职责拆成两层——纸是一层,纸上的图是另一层。
这个拆法解决了一个很实际的场景——多子图。你想在一张报告图里并排放"销售额折线"和"各区域柱状图",就得先有一张能装下两个坐标系的画布,再往里面塞两个 Axes。如果只有一层对象,这个需求根本表达不了。
💡 关键直觉:别把 Figure 和 Axes 想成两个平级的"图",它们是从属关系——Axes 是"长在" Figure 里的。画布是容器,坐标系是容器里的内容。这个主次关系一旦立住,后面所有接口都好记了。
Figure 就是整张画布。它自己不画数据,只负责"撑场面"——定尺寸、定底色、定分辨率,以及提供一个让 Axes 挂上去的地方。
创建一个 Figure,最直接的方式是 plt.figure(),它会返回一个 Figure 对象。但更常用的其实是 plt.subplots(),因为后者在创建画布的同时,顺手就帮你挂好了一个 Axes——对"画一幅图"这个最常见需求来说,它一步到位。
import matplotlib.pyplot as plt fig = plt.figure(figsize=(8, 6)) # 一张 8×6 英寸的画布 ax = fig.add_subplot(111) # 在画布上挂一个坐标系 ax.plot([1, 2, 3], [1, 4, 2])
上面这段和 plt.subplots() 做的事本质一样,只是拆得更细,把"建画布"和"挂坐标系"分成了两步。拆开看,你才能看清 Figure 的职责。
Figure 身上有几个最常用的属性值得现在记一下:figsize 定画布的长宽(单位是英寸),dpi 定每英寸的像素点数(影响清晰度),facecolor 定背景色。这些名字在第 1.5 节保存图像时还会反复出现。
⚠️ 常见坑:
figsize的单位是英寸,不是像素,也不是厘米。很多人想"把图调成 800 像素宽",结果写了个figsize=(800, 600),得到一张巨大的画布。真正的像素宽,是figsize的英寸数乘上dpi算出来的。
Axes 是真正干活的地方。你所有绘图函数——画线、画点、画柱——最终都作用在某个 Axes 上。一个 Axes 里包含了一整组元素:横轴、纵轴、标题、轴标签、刻度、图例、以及画出来的数据图形本身。
这里必须先拆一个让无数人栽跟头的词:Axes 和 Axis 不是一回事。
你可以这样记:一个 Axes 里面有两条 Axis。ax.set_title 是给整个 Axes 设标题,ax.set_xlabel 是给其中一条 Axis 设标签。前者是"框"的级别,后者是"边"的级别。
import matplotlib.pyplot as plt fig, ax = plt.subplots() ax.plot([1, 2, 3], [2, 1, 3]) ax.set_title("标题挂在 Axes 上") # 整个坐标系区域 ax.set_xlabel("这是 x 轴(Axis)") ax.set_ylabel("这是 y 轴(Axis)") ax.set_xlim(0, 4) # 控制一条轴的范围 ax.set_ylim(0, 4)
这段代码里,set_title 作用在 Axes 整体上,set_xlim 和 set_ylim 则分别作用在两条 Axis 上。把这两类方法分清,你就不会再把"设标题"和"设轴"混在一起。
Figure 和 Axes 之外,还有第三个词会反复出现——Artist。它是 Matplotlib 里所有可见元素的基类:一条折线是一个 Artist,一个标题文本是一个 Artist,一个刻度线也是一个 Artist。
它不像 Figure、Axes 那样是"一个具体的对象",而是一个"类别"——Figure 和 Axes 本身其实也是 Artist 的特例。这张层级图能帮你看清三者的关系:
这张图的信息量很大,但核心就一条:从 Figure 往下,一层层都是 Artist。 你现在不必记住每个子元素的名字,只需要建立一个直觉——在 Matplotlib 里,你调的任何"外观",本质上都是在改某个 Artist 的属性。1.4 节讲的颜色、线型、标记,全是在给这些 Artist 设属性。
💡 关键直觉:当你看到一张图想"这个字太小了想改大点",其实是在问"这个文本 Artist 的字号属性是哪个"。Matplotlib 里几乎所有的定制,都是"找到对应的 Artist,改它的属性"。这个思维方式能让你从"死记参数"升级到"按图索骥"。
搞懂对象之后,还有一个绕不开的困惑:为什么有的教程写 plt.plot(...),有的写 ax.plot(...)?这两个都能画图,区别在哪?
答案是 Matplotlib 提供两套接口,一套快捷、一套精确:
plt.xxx):背后维护一个"当前画布、当前坐标系"的隐式状态。你连续调 plt.plot、plt.title,它都知道作用在最近那个坐标系上。写起来短,适合快速探索、画单张图。fig.xxx / ax.xxx):你手里明确握着 fig 和 ax 这两个对象,所有操作都显式指定作用对象。写起来稍长,但不会含糊,适合画多子图、做精细控制。| 维度 | pyplot 快捷接口 | 面向对象接口 |
|---|---|---|
| 写法 | plt.plot(x, y) |
ax.plot(x, y) |
| 作用对象 | 隐式,靠"当前状态" | 显式,直接指定 ax |
| 代码长度 | 短 | 略长 |
| 画单张图 | 方便 | 稍繁琐但清晰 |
| 画多子图 | 容易搞混作用对象 | 清晰,各管各的 ax |
| 推荐场景 | 快速探索、临时分析 | 正式产出、多图、可维护 |
我的建议很明确:哪怕画单张图,也优先用面向对象接口。 原因很实在——plt.plot 那套"当前状态"在只有一个图时看不出问题,一旦你要加第二张子图、或者代码要给别人维护,隐式状态就开始咬人:你以为是画到 A 图,结果画到了 B 图。ax.plot 从第一天就把"画在哪"写清楚,习惯成本极低,收益却很大。
import matplotlib.pyplot as plt # 面向对象接口:一次建 1 行 2 列,共两个坐标系 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4)) ax1.plot([1, 2, 3], [1, 4, 2]) # 明确画在左边这个 ax1 ax2.plot([1, 2, 3], [3, 1, 2]) # 明确画在右边这个 ax2 ax1.set_title("左图") ax2.set_title("右图")
这段代码里,"画在哪"这件事从对象名 ax1、ax2 上一眼就能看出来。等你在第 2 章学多子图时,这套写法会直接复用。

一句话点题:Figure 是"一",Axes 是"多"——一张画布上可以并排放多个坐标系,每个坐标系各画各的图,这就是多子图的全部秘密。
创建 Axes 的入口不止 plt.subplots 一个,Matplotlib 给了好几种,各有各的用场。把它们分清,你就能在合适的地方用合适的工具。
plt.subplots:一次创建画布加一组规整网格的 Axes,最常用。它返回 (fig, axes),单个 Axes 时 axes 是一个对象,多个时是一个数组。绝大多数场景用这一个就够。fig.add_subplot(rows, cols, index):在已有画布上按"行、列、序号"逐个加 Axes,适合已经握着画布、想慢慢往里填的情况。序号从 1 开始,按"从左到右、从上到下"数。fig.add_axes([left, bottom, width, height]):按"相对画布的比例"精确定位一个 Axes。四个数分别是左下角的横坐标、纵坐标、宽、高,都是 0 到 1 之间的小数,适合画中画、非规整布局这类要把图摆到任意位置的场景。import matplotlib.pyplot as plt fig = plt.figure(figsize=(10, 6)) ax1 = fig.add_subplot(2, 2, 1) # 2 行 2 列里的第 1 个 ax2 = fig.add_subplot(2, 2, 2) # 第 2 个 ax3 = fig.add_axes([0.6, 0.1, 0.3, 0.3]) # 在右下角自定义一个小图 ax1.plot([1, 2, 3], [1, 4, 2]) ax2.plot([1, 2, 3], [3, 1, 2]) ax3.plot([1, 2, 3], [2, 2, 2])
add_axes 平时用得不多,但它是理解"坐标是相对画布的比例"这套坐标系的好例子:left 和 bottom 定左下角位置,width 和 height 定尺寸,全部用画布宽高的比例表达。这套"相对坐标"的思维,第 4 章讲坐标变换时还会更深入地出现,现在先混个眼熟就行。
搞懂两层对象的分工后,有个细节会变得很自然——总标题和子标题是两个不同的方法。
ax.set_title 给某一个 Axes 设标题,属于"子标题"。fig.suptitle 给整张 Figure 设总标题,显示在所有 Axes 之上,属于"总标题"。当你画一张含多个子图的大图时,通常两者都要:suptitle 说明整张图在讲什么(比如"华东各城市销售对比"),每个 set_title 说明各自子图在讲什么(比如"上海""杭州")。
import matplotlib.pyplot as plt fig, axes = plt.subplots(1, 2, figsize=(10, 4)) fig.suptitle("华东销售对比(总标题)", fontsize=16) axes[0].plot([1, 2, 3], [1, 4, 2]) axes[0].set_title("上海") axes[1].plot([1, 2, 3], [3, 1, 2]) axes[1].set_title("杭州")
这个"总标题加子标题"的组合,正是 Figure 和 Axes 两层对象各自承担职责的直接体现:总标题挂在 Figure 上,子标题挂在各自的 Axes 上。能分清这两个方法分别改的是谁,就说明对象模型真正想明白了。
⚠️ 常见坑:把总标题误写成
fig.set_title。Figure 没有set_title这个常规方法,它的总标题方法叫suptitle(super title 的缩写,即"总标题")。记不住时就想:总标题在子标题之上,所以带一个"super"。
它始终是复数形式(词尾带 s),即便你只画一个坐标系,这个对象在 Matplotlib 里也叫 Axes,而不是 Axe 或 Axis。你可以把它当成一个"永远带 s"的专有名词——它指代的是"坐标系区域"这个整体,而不是某一条轴。一个坐标、多个坐标,都叫 Axes;单数的 Axis 只用来指横轴或纵轴那一条线。
默认选 ax(面向对象接口)。只有一种情况 plt 更省事:你只想临时画一张图快速看一眼,不打算保留成代码,也不会出现多图需求。除此之外,都用 ax——它从一开始就写清楚"画在哪",代码越往后、图越复杂,越不容易出错。
plt.subplots 在子图多于一个时,返回的第二个值是一个数组(而不是单个对象),数组里的每个元素对应一个 Axes,按"从左到右、从上到下"的顺序排列。你可以解包一次拿全,也可以用下标单独取。
fig, axes = plt.subplots(2, 2) # 返回 2×2 的 Axes 数组 axes[0, 0].plot([1, 2, 3], [1, 2, 3]) # 左上 axes[1, 1].plot([1, 2, 3], [3, 2, 1]) # 右下
这里 axes[0, 0] 是左上角、axes[1, 1] 是右下角,下标就是"第几行、第几列"。这个"返回值是数组"的细节,等第 2 章专门讲多子图时会成为主力写法,现在先知道有这个形态即可。
figsize 单位是英寸不是像素,真实像素由 figsize 乘 dpi 决定。下一节我们正式动手,把数据真正画进这个坐标系里——plot、scatter、bar、hist 四个最基础的绘图函数,以及它们各自适合什么样的数据。