做到真正“一张图讲清一件事”的时候图例的重要性会被瞬间放大。调色、改坐标轴、换字体这些功夫大家都会做但最后往往用legend()的默认值草草收场等图导出来才发现边框压着曲线、文字和线条挤成一团、散点图和折线图的符号比例也很别扭。这些问题既不致命却又真实拉低观感。这篇文章准备把legend()函数的参数从里到外过一遍默认调用是怎么工作的、loc和bbox_to_anchor的位置逻辑怎么组合才不翻车、外观和字体参数怎么跟不同出图风格匹配以及实际项目里最常见的几种翻车现场。内容不追求把文档抄一遍而是按我自己的使用习惯把真正能救命的参数单独拎出来讲透。1. 先搞清楚legend()到底在管什么很多人对legend()的认知停留在“加个框、列出变量名”这一层这也能解释为什么大多数图的图例都像“贴上去的”。要理解legend()先要理解它的数据来源以及它返回的那个Legend对象到底是什么。legend()本质上是在当前坐标轴上收集所有带有label属性的图形元素把这些元素对应的“句柄”handle也就是线条、色块、散点集合等对象和“标签文本”label配对再按照给定的布局规则摆放到一个独立图层上。它不等于简单的文字注释它生成的是一整套可以继续操作的图形对象。1.1 无参调用到底收集了什么最常见的写法是import matplotlib.pyplot as plt fig, ax plt.subplots(figsize(8, 5)) ax.plot([1, 2, 3, 4], label训练集) ax.plot([1, 3, 2, 4], label验证集) ax.legend()这里的ax.legend()没有任何参数Matplotlib 会自动从当前Axes对象里寻找所有“带 label 的 Artist”然后把它们生成图例。关键点在于如果没有给曲线设置label那legend()可能什么都收集不到不会报错但也只会得到一个空图例框。另一个被经常误用的做法是给legend()直接传字符串列表ax.plot([1, 2, 3, 4]) ax.plot([1, 3, 2, 4]) ax.legend([训练集, 验证集])这种写法也能出来图例但背后的逻辑是“按当前位置顺序把字符串依次分配给已有图形元素”。一旦你之后调整了绘图顺序或者在中间插入了一根不需要进图例的辅助线字符串列表和曲线的对应关系就会错位图例上显示的名称会张冠李戴。我个人的建议非常明确尽量用label加无参legend()的组合让 Matplotlib 自己收集避免人为维护顺序。如果手动传handles和labels也必须保证两边的顺序严格对应。1.2legend()管的是三层内容要记住legend()的所有参数不需要死记只需要理解它管三个层面。第一层是“内容层”解决哪些元素进图例、元素的顺序、元素的句柄样式。涉及handles、labels、numpoints、scatterpoints、markerscale等参数。第二层是“位置层”解决图例放在坐标系内还是坐标系外、跟哪个锚点对齐、图例项如何排列。涉及loc、bbox_to_anchor、ncol、mode等参数。第三层是“外观层”解决边框、背景、圆角、阴影、字号等视觉细节。涉及frameon、facecolor、edgecolor、fancybox、shadow、fontsize等参数。理解了这三层后面不管遇到什么图例需求都能立刻定位到对应参数不会一上来就乱调。2. 第一梯队位置与锚定逻辑loc与bbox_to_anchor图例放在哪永远是第一个要解决的痛点。loc负责选择“对齐基准点”bbox_to_anchor负责把这个基准点钉到某个具体坐标上。两者必须配套理解只调其中一个经常会得到莫名其妙的位置。2.1loc参数从字符串到数字编码loc最常见的取值是这些字符串数字编码含义best0自动选择重叠较少的区域upper right1右上角upper left2左上角lower left3左下角lower right4右下角right5右侧居中center left6左侧居中center right7右侧居中lower center8底部居中upper center9顶部居中center10正中心数字编码是从旧版本保留下来的现在写字符串可读性更好但偶尔会看到老代码用loc1这种写法知道含义即可。这里有个容易被忽略的点best并不是真正全局最优它只是通过一个启发式算法去寻找“重叠面积较小”的区域。算法本身不差但它不会考虑你的业务重点。比如你的曲线右上方是数据密集区左下方反而是空白算法可能因为“总重叠面积小”选到右上方结果把关键曲线挡掉了。所以实际项目里不要过于依赖best尤其是当你的图例项很多、数据分布又不规则时手动指定角落位置更可控。2.2bbox_to_anchor把图例放到坐标轴外面bbox_to_anchor是legend()中最强大也最容易被误解的参数。它的核心作用是把loc指定的那个图例“角”钉到bbox_to_anchor指定的点上。举一个高频场景把图例放到坐标轴外部的右上方。fig, ax plt.subplots(figsize(8, 5)) ax.plot([1, 2, 3, 4], label方案A) ax.plot([1, 3, 2, 4], label方案B) ax.legend(bbox_to_anchor(1.02, 1), locupper left, borderaxespad0)这段代码的效果是图例的左上角对齐到坐标为(1.02, 1)的点上。这个坐标默认是相对于当前坐标轴比例的1就代表坐标轴右上角1.02表示在坐标轴右侧多出的 2% 宽度处。所以整体图例在坐标轴外侧且靠着右上角。如果不加loc只写bbox_to_anchor(1.02, 1)图例的哪个角来对齐这个点并不明确结果容易漂移。所以loc指定“图例自己的哪个角”bbox_to_anchor指定“角要贴到哪个位置”两者是绑定的。当图例在坐标轴外时还要注意保存导出问题。直接savefig()会因为图例超出画布而被裁掉最省事的处理方式是fig.savefig(result.png, bbox_inchestight, dpi300)或者预先给右侧留出空间fig.subplots_adjust(right0.75)如果是普通展示不导出那用tight_layout有时候也能自动调整但可靠程度不如subplots_adjust。2.3 四元组加上modeexpand制造横跨整行的图例bbox_to_anchor除了传二元组还可以传四元组格式是(x, y, width, height)。这时候它描述的是一个虚拟矩形典型场景是放在坐标轴上方当“顶部标题行”ax.legend( bbox_to_anchor(0, 1.02, 1, 0.12), loclower left, modeexpand, ncol3, borderaxespad0 )这里的(0, 1.02, 1, 0.12)表示从坐标轴最左侧开始往上 1.02 倍高度处横向覆盖整个宽度高度 0.12。配合loclower left表示图例底部贴近这个矩形的底边modeexpand则让图例在水平方向拉伸把整块宽度占满。这个组合常用于三个图例项横排成一行像小标题一样挂在图上方比默认挤在右上角清爽得多。3. 第二梯队外观与样式参数边框、阴影、圆角、透明度位置定好了接着就是让图例和整张图的风格统一。这里的参数数量不少但都直白掌握核心组合就能应付大多数场景。3.1 边框、圆角、阴影、透明度的完整配合默认图例带一个浅色底框但这个框放在出版图或者比赛图上往往显得突兀。常见的微调方式leg ax.legend( frameonTrue, facecolorwhite, edgecolor#333333, framealpha0.9, fancyboxFalse, shadowFalse )frameonFalse不要框完全融入画布。适合底色很干净的图。facecolornone框透明只留边界线。framealpha0.9背景透明度0 为全透明1 为完全不透明。建议不设成 1留一点透气感。fancyboxTrue圆角边框默认是闭合的更适合偏活泼风格。shadowTrue给图例加投影视觉上像浮起来但这种效果在现代风格里很容易显得脏。经验之谈科研论文的图推荐fancyboxFalse加细边框做演示PPT或者业务报表可以开圆角但阴影最好别乱用。3.2 字号与字体调整的两个入口控制图例文字的常见参数是fontsize也有prop这种更底层的写法ax.legend(fontsize12) # 等价写法 ax.legend(prop{size: 12})fontsize还可以传描述性字符串比如small、medium、large分别对应一组预设字号。但中文场景下光调字号不够字体族经常要一起指定。Windows 下的宋体、黑体macOS 下的苹方名字各不相同。如果不统一Matplotlib 会回退到默认字体中文全部变成方框。推荐在画图前统一配置plt.rcParams[font.sans-serif] [Microsoft YaHei, SimHei, Arial] plt.rcParams[axes.unicode_minus] False第二行的axes.unicode_minus是必加项否则坐标轴的负号会变成乱码方块。字体族在prop里也可以单独设置适合只需要图例换字体的场景。3.3 用Patch和Line2D自己制造图例样式有时候数据本身并不需要画出真实图形只是想要一个“说明色块的图例”。比如你画的是柱状图希望图例里显示背景说明色块而不是柱体本身。这时候可以手工制造样本from matplotlib.patches import Patch legend_elements [ Patch(facecolor#2E86AB, label类别A), Patch(facecolor#A23B72, label类别B), ] ax.legend(handleslegend_elements)折线类型也可以用类似方式from matplotlib.lines import Line2D legend_elements [ Line2D([0], [0], color#2E86AB, lw2, label方案A), Line2D([0], [0], color#A23B72, lw2, linestyle--, label方案B), ] ax.legend(handleslegend_elements)这种手工构造的方案在论文插图里很实用尤其是当图形本身太复杂直接用真实句柄会连带着把无关样式也带进图例。4. 第三梯队图例内容控制句柄、标签、筛选与重排图例内容不是“画了哪些曲线就显示哪些曲线”这么简单。实际项目中经常要手动控制哪些进图例、哪些不进以及进图例后的排列顺序。4.1handles和labels手动指定时的顺序陷阱手动传参的完整写法line1, ax.plot([1, 2, 3, 4], label训练集) line2, ax.plot([1, 3, 2, 4], label验证集) ax.legend(handles[line1, line2], labels[训练集, 验证集])这里有个新手常踩的坑ax.plot()返回的是一个Line2D列表即使画一条线返回的也是一个长度为 1 的列表。如果不加逗号解包直接写line1 ax.plot(...)那line1就变成列表了传给handles时会出现奇怪的嵌套问题。正确写法就是上面那样加逗号line1, ax.plot(...)。当然实际项目里往往不需要手动取变量可以用get_legend_handles_labels()一次性取回handles, labels ax.get_legend_handles_labels() ax.legend(handles, labels)这个方法返回两个列表顺序与当前图例顺序一致可以方便地对它们做切片、倒序、合并。4.2 用get_legend_handles_labels()做筛选和合并多子图场景下特别有用。假设一个画布里有四个子图你希望把四个子图的图例统一收集到整个画布的最上方而不是每个子图各画一个。合并写法all_handles [] all_labels [] for a in axes: handles, labels a.get_legend_handles_labels() all_handles.extend(handles) all_labels.extend(labels) fig.legend(all_handles, all_labels, locupper center, ncol4, bbox_to_anchor(0.5, 1.02))注意如果多个子图中的对象有重复标签这样直接extend会造成图例里出现重复项。需要在合并前做一次去重或者用字典以 label 为键去重。筛选也一样。只想要前两个对象的图例直接切片handles, labels ax.get_legend_handles_labels() ax.legend(handles[:2], labels[:2])这个逻辑比在绘图时控制label更灵活因为它不要求你提前决定哪些曲线要显示。4.3 用_nolegend_标记隐藏曲线如果不希望某条线出现在图例里又不想改动整套handles筛选逻辑最省事的办法是给这条线设置特殊标签ax.plot(x, noise, label_nolegend_)只要 label 是以下划线开头的字符串Matplotlib 就会默认忽略这条线不把它收进图例。这是个约定俗成的机制比“不设置label”更显式因为你知道这条线原本有名字只是不想展示。另一个隐蔽技巧是给曲线手动设置set_label()空字符串在某些版本的 Matplotlib 中会被当作“没有标签”处理但可靠程度不如_nolegend_所以建议统一使用下划线开头的方式。4.4 散点、折线混编时的符号控制当图例里既有折线又有散点时符号大小和数量需要单独调否则可能出现折线图例里显示两个点密密麻麻而散点图例则点太小看不清的情况。numpoints控制折线图例中显示的标记点数默认是 2一般设成 1 更干净。scatterpoints控制散点图图例中点标的个数默认也是 1除非想表达多个点一般 1 就够。markerscale按比例放大或缩小图例里的标记尺寸数值大于 1 会放大。ax.legend(numpoints1, scatterpoints1, markerscale1.2)我实际出图时markerscale经常设成 1.2 到 1.5因为图例里的符号总体偏小放大会让读者看得更清楚也不会跟正文文本抢视觉。5. 第四梯队空隙、列数与排版细节当图例项很多、或者图例需要放在横向空间有限的位置时单纯调到ncol还不够周围的间隙参数才是决定排版质感的关键。这类参数单个看都不起眼组合起来效果立竿见影。5.1ncol与columnspacingncol控制图例列数。比如你要让四个图例项排成两排就设ncol2要横排成一行就设ncol4ax.legend(locupper center, bbox_to_anchor(0.5, 1.08), ncol4, columnspacing1.0)columnspacing是列与列之间的水平间距单位为字体大小比例。默认值是 1在字体较大时两列文字之间可能贴得太紧。这种情况下可以稍微调到 1.2 或 1.5。顺带提醒列数不是越多越好。当ncol超过 3 或 4每个图例项的可读宽度会变小长变量名会被截断所以我一般只在变量名较短时用多列布局。5.2labelspacing、handlelength、handletextpad、borderaxespad这四个参数控制图例内部和外部的间隙参数控制对象默认值经验值labelspacing条目之间的垂直间距0.50.6 到 1.0handlelength句柄线条的长度2.01.5 到 2.5handletextpad句柄与文字之间的水平距离0.81.0 到 1.4borderaxespad图例框与坐标轴之间的间距0.5图例置外时设 0我的习惯是条目比较多时labelspacing调大一点避免文字行距挤成一条线条型数据比较多时handlelength缩短不让那条示例线拖得太长而图例放在坐标轴外时borderaxespad0可以让图例严丝合缝地靠在坐标轴边缘不留白边。这组间距单位都是以当前字体大小为基准的“相对值”所以字体改大之后不需要重新调它们会自动跟随变大。5.3title与对齐方式图例也可以有标题用title参数ax.legend(title指标说明, title_fontsize10, labelspacing0.5)title_fontsize默认会继承图例字号但单独显式设置更稳。标题出现后图例的整体高度会增加放在坐标轴外时要留足空间。对齐方式参数alignment在较新版本的 Matplotlib 中可用取值为left、center、right。它控制图例中文本块的对齐方式对于多列图例尤其好用。6. 实际项目里的高频组合与进阶玩法这里整理几个我反复使用的场景组合基本覆盖了大部分真实需求。6.1 右侧外部图例与版面的配合这是做机器学习模型对比时最常用的版式之一图例放在坐标轴右侧正文部分占满剩余空间。fig, ax plt.subplots(figsize(8, 5)) ax.plot(x, y1, label模型A) ax.plot(x, y2, label模型B) ax.plot(x, y3, label模型C) ax.legend(bbox_to_anchor(1.02, 1), locupper left, borderaxespad0) fig.subplots_adjust(right0.75)调整right0.75的目的是让坐标轴本身在画布中只占 75% 的宽度剩下 25% 留给右侧图例。如果不做这一步图例虽然画出来了但坐标轴会延伸到图例下面两者叠在一起。6.2 多子图的共享图例多子图共享一个图例时要避免使用plt.legend()因为它只会处理“当前坐标系”。正确的思路是先用循环把所有子图的句柄收集起来再传给fig.legend()。合并时对handles和labels做顺序保持和去重unique_handles [] unique_labels [] for a in axes: handles, labels a.get_legend_handles_labels() for h, l in zip(handles, labels): if l not in unique_labels: unique_handles.append(h) unique_labels.append(l) fig.legend(unique_handles, unique_labels, locupper center, ncol4)这样图例不会出现同一名字的重复项顺序也跟随第一次出现的顺序。6.3 让图例可以拖动、移除legend()返回的Legend对象是可交互的。如果做数据探索性分析希望手动拖动图例位置只需leg ax.legend() leg.set_draggable(True)这行代码之后图例就可以通过鼠标直接拖到任何位置非常适合在 Jupyter 里快速尝试不同位置。同样的对象也支持leg.remove()在画布上彻底删除图例而不需要重新绘图。对事件绑定比较熟练的读者还可以监听图例点击事件实现“点击图例项隐藏曲线”这类交互这已经不是legend()参数本身能覆盖的但确实建立在理解handles对象结构的基础上。7. 常见问题速查与排错记录最后把我在实际使用中踩过的坑和排查思路整理成一张表算是一个速查工具。现象大概率原因解决办法调了legend()但图例空白曲线没有设置label且没有手动传handles给曲线加label或手动传handles图例名称和曲线顺序对不上使用字符串列表传参顺序错位改用label加无参legend()导出图片时图例被裁掉图例在坐标轴外但没预留空间savefig(bbox_inchestight)或subplots_adjust(right0.75)图例文字出现方框乱码中文字体未配置设置rcParams[font.sans-serif]图例里出现多余的线辅助线也设置了label辅助线设label_nolegend_多个子图出现多个图例在每个子图里都调了legend()合并handles后统一用fig.legend()图例覆盖了关键数据locbest算法选择的位置不理想手动指定loc并用bbox_to_anchor微调折线图例里点太多numpoints未设置设numpoints1图例字号太小或太大fontsize未设置显式设置fontsize不要依赖默认这些坑大多不是“不知道参数”而是“参数组合时机不对”。排错时我通常先判断是内容层的问题、位置层的问题还是外观层的问题再针对性检查对应参数效率会高很多。最后说一个我自己的习惯我画完图后一定会做一步“图例检查”把图例四个角落各放一遍用眼睛确认遮挡情况。因为很多参数在静态代码里看不出问题只有真正渲染出来才发现borderaxespad多留了那么一点空白或者ncol多了一列导致整体偏宽。图例这件事调参不难难的是愿意花时间多看几眼渲染结果。希望这篇文章能帮你把legend()从“默认能用”推进到“顺手可控”的程度。