4.1 Artist 对象详解


文档摘要

4.1 Artist 对象详解 本节摘要:Artist 是 Matplotlib 里所有可见元素的统一基类——一根线条、一行文字、一个坐标轴、甚至整张画布,全都是 Artist。它分两类:容器型 Artist 负责装别的 Artist(Figure、Axes、Axis),图元型 Artist 负责真正被画出来(Line2D、Text、Patch)。它们组成一棵以 Figure 为根的树,渲染器遍历这棵树来出图。掌握 Artist,你就拿到了直接操纵图中任意元素的钥匙,而不是只能通过 plot、title 这些高层函数间接操作。 本节地图 阅读完本节,你应当能够: 说清"一切皆为 Artist"的含义,并区分容器型与图元型 Artist。

4.1 Artist 对象详解

本节摘要:Artist 是 Matplotlib 里所有可见元素的统一基类——一根线条、一行文字、一个坐标轴、甚至整张画布,全都是 Artist。它分两类:容器型 Artist 负责装别的 Artist(Figure、Axes、Axis),图元型 Artist 负责真正被画出来(Line2D、Text、Patch)。它们组成一棵以 Figure 为根的树,渲染器遍历这棵树来出图。掌握 Artist,你就拿到了直接操纵图中任意元素的钥匙,而不是只能通过 plot、title 这些高层函数间接操作。

本节地图

阅读完本节,你应当能够:

  1. 说清"一切皆为 Artist"的含义,并区分容器型与图元型 Artist。
  2. 画出 Figure、Axes、Axis、Line2D、Text 之间的父子关系。
  3. 通过 ax.lines、ax.texts、ax.get_children 等入口拿到已有对象。
  4. 用 set_ 方法和 set 方法安全地修改对象的颜色、线宽、透明度等属性。
  5. 理解 zorder 如何决定图层的前后遮挡。
  6. 说出为什么要用 set 方法而不是直接给属性赋值。

一、先问一个让你怀疑的问题

我们画图这么多年,一直以为自己在"调函数"。其实不是。你敲下 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:管事的和干活的

把 Artist 家族往下分一层,会看到两个阵营。我用"剧场"来类比:一类是舞台部门,负责搭台、划分区域、管理演员;另一类是演员,负责在台上真正亮相。

  • 容器型 Artist:自己不直接呈现视觉效果,专职收纳、组织别的 Artist。Figure 是最大的容器,装整个画布;Axes 是子图容器,装坐标轴和图形元素;Axis 是坐标轴容器,装刻度和轴脊;还有 Legend、Title 这类小容器。
  • 图元型 Artist:真正被画出来的那个。Line2D 是折线,Text 是文字,Rectangle 是矩形,Patch 是更通用的二维形状,Collection 是一次画一大批同类图元的集合。

一张表把它们的主要成员摆开:

类别 代表类 职责 是否直接上画布
容器型 Figure 顶层画布,装多个 Axes 否,只做骨架
容器型 Axes 子图,装坐标轴与图元 否,划定绘图区
容器型 Axis 单条坐标轴,装刻度与轴脊 否,管理刻度
图元型 Line2D 折线、曲线
图元型 Text 文字、标签、标题
图元型 Patch 矩形、圆、多边形等填充形状
图元型 Collection 散点、柱体等批量图元

为什么要分这两类?因为渲染的时候,容器只负责"往下走",图元才负责"落笔"。渲染器从 Figure 出发,顺着容器一路找到叶子上的图元,才真正调用绘制指令。搞清这个分工,你就明白为什么改一个 Axes 的标题和改一根线的颜色,走的是两条不同的路:前者是在容器上改一个子对象,后者是在图元上直接改自身的属性。

还有两个常被忽略的容器:Legend(图例)和 Colorbar(色条)。它们内部也装着一堆小对象——图例里每个条目是一个小 Patch 加一个 Text,色条里是一条色带加刻度。所以改图例样式,本质也是钻进 Legend 这个容器里改它的子对象。别以为图例是"画上去的一行字",它是一棵小树。

三、Artist 树:谁装谁,一目了然

容器套容器,图元挂叶子,最终长成一棵树。树根是 Figure,树干是 Axes,Axes 再分出两支——一支往下接 Axis(坐标轴,Axis 下面还有 Tick 和 Spine),另一支直接挂着 Line2D、Text、Patch 这些图元。这套层级是 Matplotlib 一切绘图的基础,后面两节的坐标变换和 Path 都要在这棵树上找位置。

图:Artist 对象层级

图:Artist 对象层级

这张图想强调一件事:继承是"类型"上的关系,包含是"实例"上的关系。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() 当成"透视眼"。遇到任何"这个元素到底是哪个对象"的疑问,先跑一遍它,看到类名,你就知道该去哪个列表里捞它了。

五、改属性:set 方法才是正道

拿到对象只是第一步,改它才是目的。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 是图例里显示的名字;colorlinewidthlinestylemarker 控制线的外观;transform 控制坐标变换,这是下一节的主角。

六、zorder 与显隐:图层的前后

两张图叠在一起,谁盖谁?看 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。做法是继承 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() 内部触发它),事情是这样发生的:

  1. 从 Figure 出发,深度优先遍历整棵 Artist 树。
  2. 每遇到一个容器,就往下走;遇到图元,检查它是否可见,可见才调用它的 draw(renderer)
  3. 每个 draw 方法内部,又会去调渲染器的具体指令——画线、画字、画形状、画图。
  4. 同一层里,按 zorder 从小到大依次画,大的后画,自然盖在上面。

理解这条链,很多现象就说得通了。为什么隐藏一个容器,它下面所有图元一起消失?因为遍历到它时发现不可见,整棵子树都被跳过。为什么改 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",其实就是它替你记着的一个对象引用。这层壳用起来省事,但对象藏在幕后,你没法精确指认;想精细操纵,就得绕过壳、直接拿对象。

要点串联

  • 一切皆为 Artist:线条、文字、坐标轴、画布,全是 Artist 的实例。
  • 两类 Artist:容器型负责装(Figure、Axes、Axis),图元型负责画(Line2D、Text、Patch)。
  • 一棵树:Figure 是根,Axes 是干,Axis 与图元是枝与叶,渲染器按树遍历。
  • 入口要熟:ax.lines、ax.texts、ax.patches 拿常见图元,get_children 看全量子树。
  • 改属性走 set:set_color、set、set_visible,别直接给属性赋值。
  • zorder 管前后:数值大的画上面,遮挡问题先查它。
  • 自定义是后话:先学会用 Patch 和 Path 拼形状,别急着继承 Artist。

下一节我们把"对象放在哪"这个问题单独拎出来讲。你会看到,同一个数据点,在不同坐标系里的坐标天差地别——这就是坐标变换要解决的事。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U