4.1 Artist 对象详解 本节摘要:Artist 是 Matplotlib 里所有可见元素的统一基类——一根线条、一行文字、一个坐标轴、甚至整张画布,全都是 Artist。它分两类:容器型 Artist 负责装别的 Artist(Figure、Axes、Axis),图元型 Artist 负责真正被画出来(Line2D、Text、Patch)。它们组成一棵以 Figure 为根的树,渲染器遍历这棵树来出图。掌握 Artist,你就拿到了直接操纵图中任意元素的钥匙,而不是只能通过 plot、title 这些高层函数间接操作。 本节地图 阅读完本节,你应当能够: 说清"一切皆为 Artist"的含义,并区分容器型与图元型 Artist。
本节摘要:Artist 是 Matplotlib 里所有可见元素的统一基类——一根线条、一行文字、一个坐标轴、甚至整张画布,全都是 Artist。它分两类:容器型 Artist 负责装别的 Artist(Figure、Axes、Axis),图元型 Artist 负责真正被画出来(Line2D、Text、Patch)。它们组成一棵以 Figure 为根的树,渲染器遍历这棵树来出图。掌握 Artist,你就拿到了直接操纵图中任意元素的钥匙,而不是只能通过 plot、title 这些高层函数间接操作。
阅读完本节,你应当能够:
我们画图这么多年,一直以为自己在"调函数"。其实不是。你敲下 ax.plot(x, y) 的时候,真正发生的是:Matplotlib 在背后 new 了一个 Line2D 对象,塞进了 ax 里。线条不是"画"出来的,是"造"出来再"挂"上去的。
验证一下,一分钟都不用:
import matplotlib.pyplot as plt fig, ax = plt.subplots() line, = ax.plot([0, 1, 2], [0, 1, 4]) print(type(line))
打印出来是 matplotlib.lines.Line2D。再试一个:
print(type(ax), type(fig)) print(type(ax.xaxis), type(ax.title))
你会看到 Axes、Figure、XAxis、Text 各自的名字。它们有一个共同的祖先——Artist。这句话不是宣传语,是 Matplotlib 架构的事实:画布上每一个看得见的东西,都是 Artist 的一个实例。反过来也成立:你想精确控制图里任何一个元素,本质上都是在操作某个 Artist 对象。
这个事实有什么用?它把画图从一条单向的命令流,变成了一个可以来回读写的对象模型。图一旦画出来,你可以事后把它拆开、找到其中某一根线、改它的颜色、再重新渲染。高层函数做不了这么细的活——你改完参数想再改,往往只能重新画一遍。
把 Artist 家族往下分一层,会看到两个阵营。我用"剧场"来类比:一类是舞台部门,负责搭台、划分区域、管理演员;另一类是演员,负责在台上真正亮相。
一张表把它们的主要成员摆开:
| 类别 | 代表类 | 职责 | 是否直接上画布 |
|---|---|---|---|
| 容器型 | Figure | 顶层画布,装多个 Axes | 否,只做骨架 |
| 容器型 | Axes | 子图,装坐标轴与图元 | 否,划定绘图区 |
| 容器型 | Axis | 单条坐标轴,装刻度与轴脊 | 否,管理刻度 |
| 图元型 | Line2D | 折线、曲线 | 是 |
| 图元型 | Text | 文字、标签、标题 | 是 |
| 图元型 | Patch | 矩形、圆、多边形等填充形状 | 是 |
| 图元型 | Collection | 散点、柱体等批量图元 | 是 |
为什么要分这两类?因为渲染的时候,容器只负责"往下走",图元才负责"落笔"。渲染器从 Figure 出发,顺着容器一路找到叶子上的图元,才真正调用绘制指令。搞清这个分工,你就明白为什么改一个 Axes 的标题和改一根线的颜色,走的是两条不同的路:前者是在容器上改一个子对象,后者是在图元上直接改自身的属性。
还有两个常被忽略的容器:Legend(图例)和 Colorbar(色条)。它们内部也装着一堆小对象——图例里每个条目是一个小 Patch 加一个 Text,色条里是一条色带加刻度。所以改图例样式,本质也是钻进 Legend 这个容器里改它的子对象。别以为图例是"画上去的一行字",它是一棵小树。
容器套容器,图元挂叶子,最终长成一棵树。树根是 Figure,树干是 Axes,Axes 再分出两支——一支往下接 Axis(坐标轴,Axis 下面还有 Tick 和 Spine),另一支直接挂着 Line2D、Text、Patch 这些图元。这套层级是 Matplotlib 一切绘图的基础,后面两节的坐标变换和 Path 都要在这棵树上找位置。

这张图想强调一件事:继承是"类型"上的关系,包含是"实例"上的关系。Line2D 和 Axis 都继承自 Artist,但它们挂在树上的位置完全不同——Line2D 是 Axes 直接收纳的图元,Axis 是 Axes 下面的容器。别把这两种关系混成一条线,否则你会以为"改 Axis 就是改一条线"。
这个树结构还有个实用推论:想批量操作所有折线,就去 ax.lines 里找;想批量操作所有坐标轴,就遍历 fig.axes 再各自拿 xaxis 和 yaxis。树的分支结构直接对应了这些查找入口——树长什么样,你的代码就该怎么走。
知道对象在哪,下一步是把它找出来。Matplotlib 留了好几条入口,我按"常用程度"排个序。
最顺手的是从 Axes 的现成列表里取:ax.lines 拿到所有折线,ax.texts 拿到所有文字,ax.patches 拿到所有填充形状,ax.images 拿到所有图像。这些列表是"活"的——你画一根线,它多一个元素,删掉线,它少一个。
fig, ax = plt.subplots() ax.plot([0, 1], [0, 1]) ax.plot([0, 1], [1, 0]) print(len(ax.lines)) # 2 first = ax.lines[0] # 第一根线
更彻底的是 ax.get_children(),它把 Axes 下所有子 Artist 一股脑列出来,包括那些看不见的——刻度线、刻度标签、轴脊、背景矩形。想看清一棵子树到底装了啥,这是最快的办法。
for child in ax.get_children(): print(type(child).__name__)
跑一遍你会发现,一个看似"空"的 Axes 其实早就挂了一堆对象:两个 Axis、几根 Spine、背景 Patch、标签 Text。你画的折线只是其中最新来的那一个。这个事实能解释很多"玄学":为什么空图也有边框、为什么图例位置老跟你作对——因为那些东西本来就在,只是你没意识到。
对象之间还有往回指的引用:ax.figure 指向它所属的 Figure,fig.axes 列出这个画布上所有 Axes。顺着这些引用,你能从任意一个对象出发,上下左右摸遍整棵树。需要遍历所有子图统一改某个属性时,fig.axes 比手动记一堆 ax 变量靠谱得多。
💡 关键直觉:把
ax.get_children()当成"透视眼"。遇到任何"这个元素到底是哪个对象"的疑问,先跑一遍它,看到类名,你就知道该去哪个列表里捞它了。
拿到对象只是第一步,改它才是目的。Artist 属性分两种改法:用 set_xxx() 方法,或者直接给属性赋值。
line, = ax.plot([0, 1, 2], [0, 1, 4]) line.set_color('red') line.set_linewidth(2.5) line.set_linestyle('--')
一次改多个,用万能的 set:
line.set(color='blue', linewidth=3, linestyle=':', alpha=0.6)
那能不能 line.color = 'green'?语法上能跑,官方不推荐。原因很实在:有些属性的赋值背后还挂着一串联动——比如改了线宽,可能要通知渲染器重新计算;改了 label,图例得刷新。直接赋值绕过了这些联动,图暂时看着对了,下次 draw 时可能就露馅。所以老规矩:改属性一律走 set 方法。
几个天天要用的通用属性,值得背下来:alpha 是透明度,从 0 到 1;visible 是显隐开关;zorder 是前后次序,数值越大越靠前;label 是图例里显示的名字;color、linewidth、linestyle、marker 控制线的外观;transform 控制坐标变换,这是下一节的主角。
两张图叠在一起,谁盖谁?看 zorder。它是个浮点数,默认大多数图元是 1 到 3 之间,数值大的画在上面。想让某条线"浮"到所有东西上面,就把它的 zorder 调大:
line.set_zorder(10)
zorder 不是绝对值,是比较值。你只要保证"想在上面的那个"比"想在下面的那个"大就行,具体是 5 还是 500 无所谓。常见翻车现场是:画了填充区域又画了折线,结果折线被填充盖住看不见,以为数据丢了,其实是 zorder 没调对。
显隐比 zorder 更干脆——set_visible(False) 直接把对象从画面里摘掉,但它还在树上,只是不参与渲染。想临时关掉网格、关掉某根线做对比,用它最合适,比删了重画快得多。
ax.grid(True) ax.lines[0].set_visible(False) # 临时隐藏第一根线
内置对象够用九成场景,剩下的一成,你可能想自己造一个 Artist。做法是继承 Artist,实现一个 draw(renderer) 方法,在里面调用渲染器把图形画出来。思路不复杂,但门槛不低——你不仅要懂对象模型,还得懂渲染器和坐标变换怎么协作,否则画出来位置是错的、还看不懂报错。
from matplotlib.artist import Artist from matplotlib.patches import Circle class MyCircle(Artist): def __init__(self, center, radius, **kwargs): super().__init__(**kwargs) self.center = center self.radius = radius def draw(self, renderer): circle = Circle(self.center, self.radius, facecolor='red', edgecolor='black') renderer.draw_patch(circle)
我的建议很明确:先别急着自定义 Artist。你真正想画的形状,十有八九用下一节要讲的 Patch 和 Path 就能拼出来,还不用碰渲染器这个底层。自定义 Artist 留给那些需要"全新渲染行为"的场景——比如要画一个既不是线也不是形状、渲染逻辑完全定制的东西。对大多数人,理解到"Artist 可以继承、draw 是核心方法"这一层就够了。
⚠️ 常见坑:拿到对象后直接改属性(
line.color = 'green'),而不是用set_color。短代码里看不出问题,一旦图复杂起来、触发重绘,这种绕过联动的改法就会让图和内存里的状态对不上账。养成"改属性必用 set"的肌肉记忆。
前面反复提到"渲染器遍历 Artist 树",这里把这条链摊开看一遍。当你调 fig.canvas.draw()(或者 plt.show() 内部触发它),事情是这样发生的:
draw(renderer)。draw 方法内部,又会去调渲染器的具体指令——画线、画字、画形状、画图。理解这条链,很多现象就说得通了。为什么隐藏一个容器,它下面所有图元一起消失?因为遍历到它时发现不可见,整棵子树都被跳过。为什么改 zorder 能解决遮挡?因为它直接改变了同层图元的绘制次序。为什么 draw 方法里要拿一个 renderer 参数?因为图元自己不"画",它只是把指令丢给渲染器去执行。这条链是 Matplotlib 一切绘图的最终出口,前面学的对象、属性、层级,最后都汇聚到这一次遍历里。
ax.plot 返回的是列表,为什么不是单个对象?
因为一次 plot 调用可以同时画多条线。你传两组 x 和 y,它就画两条,返回值自然是个列表。这也是为什么代码里常见 line, = ax.plot(...) 这种写法——逗号解包,把列表里的第一个(通常也是唯一一个)取出来。真要画多条,就 lines = ax.plot(...) 整个列表接住,再逐条改。
为什么我改了属性,图上没变化?
在交互式后端下,改了属性后通常要触发一次重绘才生效,比如调 fig.canvas.draw() 或 plt.draw()。脚本模式下,你改完属性再 plt.show(),一般都能看到。如果完全没反应,先检查是不是拿到了错误的实例——同一个类名可能对应好几个对象,你改的那个未必是图上显示的那个。
get_children 和 ax.lines 有什么区别?
ax.lines 只给折线这一类,而且只给"活跃"的;get_children 给全量子树,包括刻度、轴脊、背景这些你平时看不见的对象。前者是精确查找,后者是全面盘点。想找特定某类元素用前者,想摸清一棵树的结构用后者。
直接赋值属性真的会出问题吗?
不一定每次都出问题,但存在风险。有些属性赋值后需要触发内部事件才能同步到渲染,绕过 set 方法就可能漏掉这一步。短期看不出,长期是个雷。既然 set 方法写起来也没多几个字,没必要赌。
pyplot 接口和对象式接口,跟 Artist 是什么关系?
pyplot 接口是对象式接口外面套的一层"状态机"。你调 plt.plot,它先找当前激活的 Axes(没有就现造一个),再在那个 Axes 上创建 Line2D。所谓"当前 Axes",其实就是它替你记着的一个对象引用。这层壳用起来省事,但对象藏在幕后,你没法精确指认;想精细操纵,就得绕过壳、直接拿对象。
下一节我们把"对象放在哪"这个问题单独拎出来讲。你会看到,同一个数据点,在不同坐标系里的坐标天差地别——这就是坐标变换要解决的事。