目录20
文章/Matplotlib/第 36 章
样式表与配色
Matplotlib · 第 36 / 43 章
plt.style:整套换外观
一个 style 就是一串 键: 值 组成的预设,plt.style.use() 把它批量灌进 rcParams。
import matplotlib.pyplot as plt
plt.style.use('seaborn-v0_8-whitegrid') # 全局切换(名字或文件路径)
plt.style.use(['dark_background', 'grayscale']) # 列表:后面的覆盖前面的
plt.style.use({'axes.grid': True}) # 也可以直接给字典
with plt.style.context('ggplot'): # 临时生效,出栈还原
...| 用法 | 一句话 | 说明 |
|---|---|---|
use(name) |
全局切换 | 会叠加在当前 rcParams 上,不是整套替换 |
use([a, b]) |
多套叠加 | 后面的赢 |
use(dict) |
直接改键 | 相当于批量 rcParams.update() |
context(name) |
临时 | 出栈还原,异常也还原 |
available |
可用名字列表 | 列名字只用它 |
library |
{名字: RcParams} |
想查某套改了哪些键才用它 |
reload_library() |
重扫样式目录 | 放了自己的 .mplstyle 后调一次 |
最小示例
import matplotlib as mpl
import matplotlib.pyplot as plt
print(len(plt.style.available), len(plt.style.library))
# 输出: 29 29
print(sorted(set(plt.style.library) - set(plt.style.available)))
# 输出: []
print('default' in plt.style.library)
# 输出: False
plt.style.use('dark_background')
print(mpl.rcParams['axes.facecolor'], mpl.rcParams['axes.edgecolor'])
# 输出: black whiteavailable 和 library 数量一致(29 vs 29),差集是空——旧资料写的“28 vs 31,library 里多三个下划线开头的内部样式”在 3.10.7 已经不成立:_classic_test_patch、_mpl-gallery 这些下划线样式也出现在 available 里了。
'default' 不在 library 里,但 use('default') 仍然合法——它是保留名:内部展开成整套出厂默认值(取 rcParamsDefault,跳过 backend 这类黑名单键),效果接近 mpl.rcdefaults() 但不动后端。

use 是叠加,不是替换
这一点决定了“换 style 之后为什么还有个键没变”:
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rcParams['axes.grid'] = True
plt.style.use('dark_background')
print(mpl.rcParams['axes.grid'], mpl.rcParams['axes.facecolor'])
# 输出: True black ← grid 是我之前设的,dark_background 没管它
plt.style.use('grayscale')
print(mpl.rcParams['axes.grid'], mpl.rcParams['axes.facecolor'])
# 输出: True whitedark_background 只有 17 个键,grayscale 16 个——它们只管自己列出的那些键,其余的从上一个状态继承。所以同一进程里连续 use 会累积。要干净的起点,先 plt.style.use('default') 或 mpl.rcdefaults()。
各套 style 的键数差别极大:
import matplotlib.pyplot as plt
for n in ['fast', 'tableau-colorblind10', 'grayscale', 'dark_background',
'ggplot', 'seaborn-v0_8-whitegrid', 'fivethirtyeight', 'bmh', 'classic']:
print(f'{n:24s} {len(plt.style.library[n]):3d}')
# 输出: fast 3
# 输出: tableau-colorblind10 2
# 输出: grayscale 16
# 输出: dark_background 17
# 输出: ggplot 22
# 输出: seaborn-v0_8-whitegrid 25
# 输出: fivethirtyeight 26
# 输出: bmh 19
# 输出: classic 258classic 有 258 个键——它是“整套还原到 1.x 外观”,几乎每个键都要重设;tableau-colorblind10 只有 2 个键,它只想换配色循环。
列表叠加:后面的覆盖前面的
import matplotlib as mpl
import matplotlib.pyplot as plt
with plt.style.context(['dark_background', 'grayscale']):
print(mpl.rcParams['axes.facecolor'])
# 输出: white
with plt.style.context(['grayscale', 'dark_background']):
print(mpl.rcParams['axes.facecolor'])
# 输出: black顺序决定结果:[A, B] 先用 A 再用 B,冲突的键 B 赢。想让“深色底 + 我的自定义配色”,就写 ['dark_background', {'axes.prop_cycle': ...}]——字典放最后兜底。
reload_library():放了新 style 文件之后
available 是进程启动时扫盘扫出来的。把自建的 .mplstyle 丢进 <configdir>/stylelib/ 之后,不调 reload_library() 就永远看不到它:
import matplotlib as mpl
import matplotlib.pyplot as plt
print(mpl.get_configdir())
# 输出: C:\Users\wx\.matplotlib
print(len(plt.style.available))
# 输出: 29
plt.style.reload_library() # 重扫 stylelib/,不用重启进程
print(len(plt.style.available))
# 输出: 29 ← 目录里没有新文件时,数量不变完整流程是三步:把 mine.mplstyle 放进 <configdir>/stylelib/ → plt.style.reload_library() → plt.style.use('mine')。
get_configdir() 在 matplotlib 上,pyplot 上没有这个函数——写 plt.get_configdir() 是 AttributeError。这也是“为什么不能只 import matplotlib.pyplot as plt“的例子之一。
注意事项
use()是叠加,不是替换;连续use会累积,想干净起点先use('default')或rcdefaults()。- 列名字用
available;library是字典,print出来几百行,只在查“某套改了哪些键”时取单个。 'default'不在library里,但use('default')合法(保留名,展开成整套出厂值,不动backend)。- 自建 style 要放
get_configdir()/stylelib/并reload_library()。 plt.style.context能吃列表和字典;mpl.rc_context只能吃字典。
导出与复用自己的样式
rcParams 改到满意之后,怎么变成可复用的 .mplstyle?不能用 savefig:
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
try:
fig.savefig('X_one.mplstyle')
except ValueError as e:
print('ValueError', str(e)[:64])
# 输出: ValueError Format 'mplstyle' is not supported (supported formats: eps, jpeg
plt.close(fig)也没法直接把 rcParams 打印出来当文件用——repr() 出来的是 Python 字典,不是 ini:
import matplotlib as mpl
from pathlib import Path
Path('X_repr.mplstyle').write_text(repr(mpl.rcParams), encoding='utf-8')这个文件共 367 行,挂上去会刷出 322 条 Bad key + 45 条 Missing colon(repr 里键带引号、长列表值跨行),367 行一个键都没被接受——len(rcParams) 仍是 322,等于什么都没读进去。它不是“报错停止”,而是“逐行跳过、静默失败”,最坑的一种。
正确做法是自己遍历,只写该写的键:
import enum
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path
# 与"样式"无关、写回去会破坏解析的键,必须排除
SKIP = {'backend', 'backend_fallback', 'timezone', 'interactive', 'toolbar',
'figure.hooks', 'figure.max_open_warning', 'webagg.address', 'webagg.port',
'webagg.port_retries', 'webagg.open_in_browser', 'tk.window_focus',
'figure.raise_window', 'date.epoch', 'docstring.hardcopy', 'savefig.directory'}
def _fmt(v):
if isinstance(v, enum.Enum): # CapStyle.butt → butt
return v.name
if isinstance(v, str):
return v[1:] if v.startswith('#') else v # '#' 是 ini 的注释符
return str(v)
def dump_style(path, **overrides):
"""把当前 rcParams 里的样式类键导出成 .mplstyle(ini 格式)。"""
mpl.rcParams.update(overrides)
lines = []
for k, v in mpl.rcParams.items():
if k.startswith('_') or k in SKIP or isinstance(v, dict):
continue
if v is None or (isinstance(v, (list, tuple, str)) and len(v) == 0):
continue # 空值写回去会变成字符串
if k == 'axes.prop_cycle': # Cycler:既不是字符串也不是列表
cols = [str(c).lstrip('#') for c in v.by_key()['color']]
lines.append("axes.prop_cycle: cycler('color', [%s])"
% ", ".join(f"'{c}'" for c in cols))
elif isinstance(v, (list, tuple)):
lines.append(f'{k}: {", ".join(_fmt(x) for x in v)}')
else:
lines.append(f'{k}: {_fmt(v)}')
Path(path).write_text('\n'.join(lines) + '\n', encoding='utf-8')
return len(lines)
n = dump_style('X_house.mplstyle',
**{'axes.grid': False, 'axes.spines.top': False, 'font.size': 12,
'figure.figsize': [8, 5], 'axes.unicode_minus': False})
mpl.rcdefaults()
plt.style.use('X_house.mplstyle') # 写出的文件必须能零告警读回
print(n, mpl.rcParams['font.size'], mpl.rcParams['axes.grid'],
mpl.rcParams['figure.figsize'], mpl.rcParams['grid.color'])
# 输出: 291 12.0 False [8.0, 5.0] #b0b0b0四条序列化规则,缺一条就会读出告警或静默失效:
- 列表值不加方括号,用逗号拼平(
k: v1, v2)。加了方括号会被当成普通字符吃进去。 - 字符串里的
#要去掉。ini 里#是注释起始符,grid.color: #b0b0b0会被截成空值;写b0b0b0就行,读回时validate_color会自动补上#——所以上面打印出来的是'#b0b0b0'。 - 枚举取
.name。lines.dash_capstyle是CapStyle.butt枚举,str()得到'CapStyle.butt',读回时报not a valid CapStyle;要写butt。 Cycler单独处理:它既不是字符串也不是列表,得拼成cycler('color', ['1f77b4', ...])这种构造表达式。
还有一条:跳过 None 和空容器。path.effects 默认是 [],写成 path.effects: [] 会被解析成含一个空字符串的列表,然后每次画图都炸(下面有实测)。
这四条都是实测撞出来的,第一版 dump_style 少了后三条,导出的文件读回时刷出 6 条 Bad value:axes.prop_cycle 被 # 截断成“unterminated string literal”、grid.color 变空串、四个 capstyle/joinstyle 全不认。写完必须 use 一次并检查 stderr,这是唯一可靠的验收方式。
三个“写进去就废”的键
figure.hooks 和 path.effects 是实测踩过的:
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path
Path('X_hook.mplstyle').write_text("figure.hooks: ['a:b']\n", encoding='utf-8')
plt.style.use('X_hook.mplstyle')
print(mpl.rcParams['figure.hooks'])
# 输出: ["['a:b']"] ← 整个列表被当成一个字符串
try:
plt.figure()
except ModuleNotFoundError as e:
print('ModuleNotFoundError', e)
# 输出: ModuleNotFoundError No module named "['a"
mpl.rcdefaults()
Path('X_eff.mplstyle').write_text('path.effects: []\n', encoding='utf-8')
plt.style.use('X_eff.mplstyle')
print(mpl.rcParams['path.effects'])
# 输出: ['[]'] ← 空列表变成一个空串元素
try:
fig, ax = plt.subplots()
ax.plot([1, 2])
fig.savefig('X_eff.png')
except AttributeError as e:
print('AttributeError', e)
# 输出: AttributeError 'str' object has no attribute 'draw_path'figure.hooks 的值必须是 ['模块:函数'] 这种真列表;style.use 解析时只会按逗号切普通字符串,于是 ["['a:b']"] 被整块当成一个模块名去 import,报 No module named "['a"。
path.effects 的类型是“PathEffect 或 None 的列表”,写 [] 得到 ['[]'](一个字符串 '[]'),画线时拿它当 PathEffect 用就 AttributeError。
结论:这两个键(以及 SKIP 里那些)不要导出。要改它们就老老实实写 Python。
注意事项
savefig('*.mplstyle')直接ValueError: Format 'mplstyle' is not supported。- 别把
repr(rcParams)当 style 文件:322 行Bad key+ 45 行Missing colon,且静默失效。 - 列表值不加方括号;
None和空容器全部跳过。 figure.hooks/path.effects不能靠.mplstyle设。- 只写“与该主题不同”的键更省事——几十行、能看、不会带进无关配置。
- 导出后必须
use一次并新开一张图验证;“文件写出了但键没生效”是这里的常态。
.mplstyle 语法:手写一份
手写比导出干净得多。格式就是 matplotlibrc 的 ini 语法:键: 值,注释用 # 或 ;。
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path
Path('X_mystyle.mplstyle').write_text("""\
# 手写主题:白底 + 只留左侧轴线 + 仅 y 向网格
axes.grid: True
axes.grid.axis: y
grid.color: e3e3e3
axes.spines.top: False
axes.spines.right: False
axes.titlesize: large
axes.titleweight: bold
lines.linewidth: 1.2
legend.frameon: False
figure.figsize: 8, 5
font.size: 11
font.sans-serif: Microsoft YaHei, DejaVu Sans
axes.unicode_minus: False
""", encoding='utf-8')
plt.style.use('X_mystyle.mplstyle')
print(mpl.rcParams['figure.figsize'], mpl.rcParams['axes.grid'], mpl.rcParams['font.size'])
# 输出: [8.0, 5.0] True 11.0
print(mpl.rcParams['font.sans-serif'][:2], mpl.rcParams['grid.color'])
# 输出: ['Microsoft YaHei', 'DejaVu Sans'] #e3e3e3注意 grid.color 写的是 e3e3e3 而不是 #e3e3e3——见下一小节。
# 是注释起始符:颜色写成 e3e3e3
这是 ini 格式和 Python 语法冲突最明显的一处。颜色值以 # 开头,而 # 在 ini 里开始一条注释,于是值被截成空串:
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path
mpl.rcdefaults()
Path('X_ok_color.mplstyle').write_text('grid.color: e3e3e3\n', encoding='utf-8')
plt.style.use('X_ok_color.mplstyle')
print(mpl.rcParams['grid.color'])
# 输出: #e3e3e3 ← 自动补上了 #
mpl.rcdefaults()
Path('X_bad_color.mplstyle').write_text('grid.color: #e3e3e3\n', encoding='utf-8')
plt.style.use('X_bad_color.mplstyle')
print(mpl.rcParams['grid.color'])
# 输出: #b0b0b0 ← 没生效,还是默认值(stderr 里一条 Bad value)关键在于 rc 的颜色校验器会帮你补 #:validate_color 收到 6 位字符串且不以 # 开头时,自动在前面加一个。所以 rc 文件里必须写 e3e3e3,而这不代表随便什么地方都能省 #:
import matplotlib as mpl
try:
mpl.colors.to_rgba('e3e3e3')
except ValueError as e:
print('ValueError', e)
# 输出: ValueError Invalid RGBA argument: 'e3e3e3'
print(mpl.colors.to_rgba('#e3e3e3')[:3])
# 输出: (0.8901960784313725, 0.8901960784313725, 0.8901960784313725)同一个 'e3e3e3',在 .mplstyle 里合法、在 to_rgba 里立刻报错——差别只在“这一层有没有 validate_color 兜底”。同理 plot(color='e3e3e3') 也会报错。
列表值:不要加方括号
这是本机实测最容易错的一处。matplotlib 的 ini 解析器对列表值只按逗号切,方括号不会被理解成“列表语法”,而是当成普通字符留在值里:
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path
Path('X_bad.mplstyle').write_text("font.sans-serif: ['AAA', 'BBB']\n", encoding='utf-8')
plt.style.use('X_bad.mplstyle')
print(mpl.rcParams['font.sans-serif'])
# 输出: ["['AAA'", "'BBB']"] ← 方括号和单引号都成了字符
Path('X_ok.mplstyle').write_text('font.sans-serif: AAA, BBB\n', encoding='utf-8')
plt.style.use('X_ok.mplstyle')
print(mpl.rcParams['font.sans-serif'])
# 输出: ['AAA', 'BBB']所以 font.sans-serif: ['Microsoft YaHei', 'DejaVu Sans'] 是错的,正确写法是 font.sans-serif: Microsoft YaHei, DejaVu Sans——不加方括号、不加引号。这一条和“Python 里写列表”的直觉正好相反。
figure.figsize 用逗号分隔
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path
Path('X_fig.mplstyle').write_text('figure.figsize: 8, 5\n', encoding='utf-8')
plt.style.use('X_fig.mplstyle')
print(mpl.rcParams['figure.figsize'])
# 输出: [8.0, 5.0]8 x 5、(8, 5)、[8, 5] 都不行——就是这个逗号形式。
axes.prop_cycle 要写 cycler(...)
配色循环不是一个颜色列表,而是一个 Cycler 对象,所以文件里得写它的构造表达式:
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path
Path('X_cyc.mplstyle').write_text(
"axes.prop_cycle: cycler('color', ['0072B2', 'D55E00', '009E73'])\n",
encoding='utf-8')
plt.style.use('X_cyc.mplstyle')
print(mpl.rcParams['axes.prop_cycle'].by_key()['color'])
# 输出: ['#0072B2', '#D55E00', '#009E73'] ← 不带 # 也能认颜色同样不带 #(原因见上一小节)。解析器把这个字符串当表达式求值成 Cycler;而在 Python 里设同一个键必须给 cycler 对象,给裸列表会报错:
import matplotlib as mpl
from cycler import cycler
try:
mpl.rcParams['axes.prop_cycle'] = ['#0072B2', '#D55E00']
except ValueError as e:
print('ValueError', str(e)[:60])
# 输出: ValueError Key axes.prop_cycle: Object is not a string or Cycler instan
mpl.rcParams['axes.prop_cycle'] = cycler('color', ['#0072B2', '#D55E00'])
print(mpl.rcParams['axes.prop_cycle'].by_key()['color'])
# 输出: ['#0072B2', '#D55E00']路径:名字、文件、绝对路径都行
import matplotlib as mpl
import matplotlib.pyplot as plt
from pathlib import Path
try:
plt.style.use('X_no_such_style')
except OSError as e:
print('OSError', str(e)[:72])
# 输出: OSError 'X_no_such_style' is not a valid package style, path of style file, UR
Path('X_mystyle.mplstyle').write_text('font.size: 13\n', encoding='utf-8')
plt.style.use(Path('X_mystyle.mplstyle').resolve()) # 绝对路径,最稳
print(mpl.rcParams['font.size'])
# 输出: 13.0
plt.style.use('default')use() 的查找顺序是:available 里的名字 → 当前目录下的文件 → 给了路径就按路径。报 OSError: not a valid package style, path of style file, URL of style file, or library style name 时,九成是文件真的不在(或不在 cwd),不是格式错。传 Path(...).resolve() 能省掉这类猜谜。
注意事项
- 注释用
#或;;值里不能出现#(颜色写0072B2)。 - 列表值不加方括号、不加引号,逗号分隔。
figure.figsize写8, 5。axes.prop_cycle写cycler('color', [...])。- 键名写错不会报错,只是不生效;值类型错才报错。写完一定要
use一次再读回来看。 - 相对路径依赖 cwd;脚本里建议绝对路径。
颜色循环与主题一致性
同一个“东北/华东/华南”在五张图里必须是同一个颜色。靠默认配色循环做不到——它按第几条线分配,加一条线就整体错位。要做主题一致性,得把“业务 → 颜色”这个映射写死,循环序列只是兜底。

axes.prop_cycle:循环序列
import matplotlib as mpl
print(mpl.rcParams['axes.prop_cycle'].by_key()['color'])
# 输出: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2',
# 输出: '#7f7f7f', '#bcbd22', '#17becf']默认 10 色。改成自己的 3 色后,第 4、5 条线会回到第 1、2 色——不是自动降亮度:
import matplotlib.pyplot as plt
from cycler import cycler
plt.rcParams['axes.prop_cycle'] = cycler(color=['#0072B2', '#D55E00', '#009E73'])
fig, ax = plt.subplots()
for i in range(5):
ax.plot([i, i + 1], label=f'L{i}')
print([ln.get_color() for ln in ax.get_lines()])
# 输出: ['#0072B2', '#D55E00', '#009E73', '#0072B2', '#D55E00']
plt.close(fig)颜色数量少于系列数量时,循环复用是默认行为,不会警告。这意味着两条不同的线可能同色——如果语义上要求可区分,就得自己保证颜色够用,或者干脆显式指定。
cycler 还能一次绑多个属性(颜色 + 线型 + 标记),这是“按第几条线换外观”的正确工具:
import matplotlib as mpl
import matplotlib.pyplot as plt
from cycler import cycler
cyc = cycler(color=['#0072B2', '#D55E00']) * cycler(linestyle=['-', '--'])
mpl.rcParams['axes.prop_cycle'] = cyc
fig, ax = plt.subplots()
for i in range(4):
ax.plot([0, 1], [i, i + 1])
print([(ln.get_color(), ln.get_linestyle()) for ln in ax.get_lines()])
# 输出: [('#0072B2', '-'), ('#0072B2', '--'), ('#D55E00', '-'), ('#D55E00', '--')]
plt.close(fig)* 是笛卡尔积:2 色 × 2 线型 = 4 种组合,但顺序是内层(后一个 cycler)变得快——先在同一条颜色上换完所有线型,再换下一个颜色。写成 + 则是串接。
显式映射:业务色优先
真正要“跨图一致”时,别指望循环——把映射写成一个字典,每次 plot 显式传 color:
import matplotlib as mpl
import matplotlib.pyplot as plt
BRAND = {'东北': '#0072B2', '华东': '#D55E00', '华南': '#009E73', '西南': '#CC79A7'}
mpl.rcParams['axes.prop_cycle'] = mpl.cycler(color=list(BRAND.values()))
print(mpl.rcParams['axes.prop_cycle'].by_key()['color'])
# 输出: ['#0072B2', '#D55E00', '#009E73', '#CC79A7']
fig, axs = plt.subplots(1, 2, figsize=(7, 2.4))
for ax in axs:
for i, (name, color) in enumerate(BRAND.items()):
ax.plot([0, 1], [i, i + 1], color=color, label=name)
axs[0].legend(fontsize=7, ncol=2)
axs[1].legend(fontsize=7, ncol=2)
plt.close(fig)两张图颜色完全一致,因为 color=BRAND[name] 覆盖了循环——prop_cycle 只是“没给 color 时”的兜底。把 prop_cycle 也设成同一组颜色,则是让“没显式指定的图”自动对齐主题。
调色板本身怎么选是另一件事:分类色要过色盲可辨(tableau-colorblind10、petroff10 都在 style.available 里),连续量用 viridis 这类感知均匀的 cmap。别用 jet。
注意事项
prop_cycle按“第几条线”分配,加一条线就整体错位;跨图一致必须显式color=。- 颜色不够用时循环复用,不报警——可能出现两条语义不同的线同色。
- 赋值必须是
cycler对象,裸列表报ValueError: Object is not a string or Cycler instance。 cycler(a) * cycler(b)是笛卡尔积,+是串接,别混。style.available里带colorblind字样的几套是现成的色盲友好配色,做分类色时先试它们。