引言
第五章前六篇把字符串的检索方法、切片、编码与正则讲完了,这一篇回到你每天都在写、却很少深究的一个语法面:花括号里冒号后面那串字符。第二章第五篇(《f-string 与字符串格式化》)已经给出三代格式化语法的全景与格式规约(format spec)的速览;本篇不再复述沿革,只做一件事——把格式规约这条「迷你语言」的完整语法逐段拆开、逐段实测:[[fill]align][sign][#][0][width][grouping][.precision][type]。对齐有四种、填充任意、符号有三种、千分位有两种写法、精度随类型语义大变,还有能把宽度、对齐符、精度全部交给变量的嵌套字段。文中每一行输出都是本机 Python 3.12.10 真实运行的结果,你可以逐条复现。
学完本篇,你应该能:不看文档写出对齐报表的格式串;一眼看懂 {:>12,.2f} 每一段在干什么;知道为什么格式化中文会「对不齐」;会用嵌套字段写出列宽可变的报表生成器——下一篇《nginx 日志解析器》的统计输出,靠的就是本篇的对齐与制式语法。
概念与原理
一条规约,三个入口,一个出口
Python 的格式化有三个对外入口:内建函数 format(value, spec)、str.format 的占位符 {...}、以及 f-string 的 f"{...}"。三者最终都走到同一个机制:value.__format__(spec)。也就是说,f"{42:>6}"、"{:>6}".format(42)、format(42, ">6") 执行的是同一套规约解释器,只是外层语法不同。这个设计来自 PEP 3101(高级字符串格式化,Python 3 引入 str.format 时确立):把「格式化指令」本身也做成一种小语言(format specification mini-language),由各类型的 __format__ 方法自行解释。这带来两件事:其一,f-string 与 str.format 的规约永远一致,学会一个就全都会;其二,未来任何新类型都可以自定义自己的规约语法(比如 datetime 的 %Y-%m-%d 风格),而不需要改动语言核心。
规约本身就是一个普通字符串,它可以被变量保存、跨函数传递、按条件拼接:先算好 spec = "<10",再 format(value, spec),输出逻辑与数据解耦;报表框架把列宽、精度、对齐方式都做成配置项,运行时拼出每一列的 spec——这就是「数据与呈现分离」在最小尺度上的样子。第七章讲过文件读写后,把 spec 写进配置文件的做法会自然落地。
规约的八段结构
一段规约完整的形态是:
[[fill]align][sign][#][0][width][grouping][.precision][type]
方括号表示可省略。逐段含义:fill 是填充字符、align 是对齐方式;sign 是符号位处理;# 是进制前缀开关;0 是零填充标志;width 是最小宽度(整数);grouping 是千分位分组符(, 或 _);.precision 是精度;type 是呈现类型。共八段。初学者最常搞错的是段与段之间的顺序:宽度在前、精度在后,{:,.2f} 里逗号(分组)必须写在宽度之后、精度之前——写成 {:.2,f} 直接抛 ValueError。规约的解析是从左到右按段匹配的,位置错了就语义错了。
为什么默认对齐「因类型而异」
规约里如果不写对齐符,数字默认右对齐、字符串默认左对齐。理由是惯性:数字的对齐天然以个位为基准(右对齐方便对齐小数点和进位),文本以首字为基准(左对齐符合阅读方向)。这个「默认值随类型切换」的规则是新手最容易忽视的行为——同一段规约 {:6},套在数值和字符串上,输出方向完全相反。
符号位与填充的先后关系
符号位 +/空格/- 只对数值有效;对齐符 =(等号对齐)是数值专属,它的语义是「把填充放在符号之后、数字之前」。所以 {-42:=10} 得到 - 42:负号顶在最左,空格填充夹在符号与数字之间。没有符号时 = 与 > 表现相同;有符号时两者立刻分道扬镳——这正是演示它行为的最佳场景。
操作与实现
对齐与填充:< 左、> 右、^ 中、= 等号
print(f"{42:<6}|") # 左对齐
print(f"{42:>6}|") # 右对齐
print(f"{42:^6}|") # 居中
print(f"{42:=6}|") # 等号对齐:无符号时与右对齐一样
print(f"{-42:=10}|") # 有符号时,填充插在符号与数字之间
print(f"{42:*<6}|") # 自定义填充字符 *
print(f"{42:*^6}|")
print(f"{'ab':<6}|") # 字符串同样支持三种对齐
print(f"{'ab':>6}|")
print(f"{'ab':^6}|")
42 |
42|
42 |
42|
- 42|
42****|
**42**|
ab |
ab|
ab |
四种对齐里,<、>、^ 对数值与字符串都适用;= 是数值专属(对字符串直接抛 ValueError,见易错点)。填充字符可以替换任意单个字符({、} 除外),报表里常用 = 或 - 画分隔线。
默认对齐方向与符号位
print(f"{42:6}|") # 数值:默认右对齐
print(f"{'ab':6}|") # 字符串:默认左对齐
print(f"{42:+}|{42: }|{-42:+}|{-42: }|{-42:-}") # 四种符号位
print(f"{42:>+6}|") # 宽度与符号组合:符号紧贴数字
print(f"{42:=+6}|") # 等号对齐:填充在符号之后
42|
ab |
+42| 42|-42|-42|-42
+42|
+ 42|
符号位有三种取值:+(正数也带正号)、-(默认,只有负数带符号)、空格(正数前留一个空格,让正负数符号位对齐)。工程上最常见的用法是 {:>+8,d} 这类组合:右对齐、强制符号、千分位,输出财务数字时一目了然。
# 与 0:进制前缀与零填充
print(f"{255:#x} {255:#X} {10:#b} {8:#o}") # # 加进制前缀
print(f"{42:06}|") # 0 标志:宽度内补零
print(f"{-42:08d}|") # 负号不被零覆盖
print(f"{255:08x}|")
print(f"{3.14:08.2f}|") # 与精度组合
print(f"{42:0=8d}") # 0 标志等价于 fill=0 + 对齐=
print(f"{'ab':08}") # 字符串:0 只是普通填充符
0xff 0XFF 0b1010 0o10
000042|
-0000042|
000000ff|
00003.14|
00000042
ab000000
#(井号)让 x/X/b/o 进制输出带上 0x/0X/0b/0o 前缀,这是把数值转成「可读回」的进制字面量时要用的开关。0 标志等价于 fill=0 加 align==:先放符号、再补零、最后数字——所以 -42 补零成 -0000042 而不是 00000-42。注意最后一行:对字符串,0 没有等号语义,只是普通填充符,'ab' 补成 ab000000。
千分位:, 与 _
print(f"{1234567:,}")
print(f"{1234567:_}")
print(f"{1234567.8912:,.2f}")
print(f"{-9876543:,}")
print(f"{1234567:,.0f}")
1,234,567
1_234_567
1,234,567.89
-9,876,543
1,234,567
分组符两种:逗号是会计惯例,下划线是给「需要程序再处理」的场景(int("1_234_567") 直接可用)。分组对整数和浮点都生效,且必须写在宽度之后、精度之前。
精度与类型:f / e / g / 进制 / %
print(f"{3.14159265:.2f}") # f:固定小数位
print(f"{3.14159265:.3e}") # e:科学计数法
print(f"{0.0000123456:.3g}") # g:按数值大小自适应
print(f"{12345:.4g}")
print(f"{1.23456:.3}") # 不写类型时按 g 解释:有效数字
print(f"{12345.678:.3}")
print(f"{255:d} {255:b} {255:o} {255:x} {255:X}") # 整数进制
print(f"{255:04x}") # 宽度 + 进制组合
print(f"{42:.2f}") # 整数也可以用浮点类型
print(f"{0.125:.1%} {0.125:.0%}") # 百分号:自动乘 100
print(f"{'hello world':.5}") # 字符串的精度=最大字符数(截断)
print(f"{'hello world':10.5}") # 先截断,再补宽度
3.14
3.142e+00
1.23e-05
1.234e+04
1.23
1.23e+04
255 11111111 377 ff FF
00ff
42.00
12.5% 12%
hello
hello
这一段的四个要点:其一,f 是固定小数位,e 是科学计数法,g(也是不写类型时的默认)按数值大小在两者间自适应,精度指有效数字而不是小数位——12345:g 不写精度时是 12345,写了 .4g 就变成 1.234e+04;其二,整数的 d、b、o、x、X 五种制式,配上宽度与 # 就是完整的进制格式化工具;其三,% 类型会把数值乘以 100 再带百分号——0.125 变 12.5%,这个「自动乘 100」是 % 类型最容易让人懵的地方;其四,字符串的精度语义是「最多取几个字符」,'hello world' 截成 'hello'——而且截断发生在补宽之前(10.5 得到 5 个字符右补 5 个空格)。
工程上怎么选类型?账务与对齐场景(要对齐小数点、对齐个位)用 f 固定小数位;物理量、时间间隔这类数值跨度大的场景用 g 或 e,让有效数字表达量级;比率、命中率这类「数值在 0 到 1 之间」的量,直接吃 % 自动乘 100 的语义,业务口径里约定原始值一律存小数(0.125 而不是 12.5),展示层再交给格式串——这样同一份数据既能输出 12.5%,也能输出 0.125,口径统一。
统一出口与嵌套字段
print("{:>6}".format(42)) # str.format 写法
print(format(42, ">6")) # 内建 format()
print(f"{42:>6}") # f-string:三者同一规约
print("{0:>6} {1:^6}".format(42, 7)) # 位置参数 + 各自规约
print("{name:>{width}}".format(name="py", width=8)) # 规约本身可嵌套
width = 8
align = "^"
print(f"{3.14159265:{width}.2f}") # 宽度交给变量
print(f"{3.14159265:{align}{width}.2f}") # 对齐符也可嵌套
print("{:{width}.2f}".format(3.14159265, width=8))
42
42
42
42 7
py
3.14
3.14
3.14
前四行证明三个入口的规约完全一致;后四行是嵌套字段(nested field):花括号里再套花括号,内层是变量或表达式,外层规约的其余部分照常是字面文本。f"{x:{width}.2f}" 在 width=8 时等价于 f"{x:8.2f}"。报表生成器里「列宽来自配置」时,这是标准写法。注意 f-string 的值表达式里若出现引号,Python 3.12 的 PEP 701 已放开同引号嵌套(第二章讲过),但在嵌套字段里写复杂表达式依然要克制——可读性优先。
工程合成:请求统计报表
把上面所有语法拼在一起,生成一张对齐的报表:
rows = [
("GET /api/login", 200, 0.42, "1.2 MB"),
("GET /api/search", 1290, 12.37, "8.4 MB"),
("POST /api/upload", 88, 3.91, "6.6 MB"),
]
print(f"{'path':<18}{'requests':>8}{'avg_time':>11}{'traffic':>9}")
for path, count, cost, size in rows:
print(f"{path:<18}{count:>8,d}{cost:>10.2f}s{size:>9}")
print(f"{'TOTAL':<18}{sum(r[1] for r in rows):>8,d}")
path requests avg_time traffic
GET /api/login 200 0.42s 1.2 MB
GET /api/search 1,290 12.37s 8.4 MB
POST /api/upload 88 3.91s 6.6 MB
TOTAL 1,578
每一列各用各的对齐:路径左对齐、请求数右对齐加千分位、平均耗时右对齐两位小数(后面补一个字面 s)、流量整列右对齐。表头与数据行用同一组宽度,列自然对齐——这就是下一篇日志统计输出所用的排版手法。
易错点与陷阱
陷阱一:默认对齐方向随类型切换
f"{42:6}" 是 ' 42',f"{'ab':6}" 是 'ab '——同一个 6,方向相反。写报表时混用数值列与文本列,若有一列忘写对齐符,整张表就错位。规则:显式写 < 或 >,永远不要依赖默认值。
陷阱二:= 对齐与符号位是数值专属
对字符串使用 = 或 + 会直接抛 ValueError,实测如下:
try:
f"{'ab':=8}"
except ValueError as e:
print('等号对齐作用于字符串:', e)
try:
f"{'ab':+8}"
except ValueError as e:
print('符号位作用于字符串:', e)
等号对齐作用于字符串: '=' alignment not allowed in string format specifier
符号位作用于字符串: Sign not allowed in string format specifier
出错信息虽然直白,但运行时才暴露,且出现在 f"{'ab':=8}" 这种看起来无害的写法上——动手前先确认目标类型是数值还是字符串。
陷阱三:精度的语义随类型三变
同一段 .precision:对浮点是「小数位」(f)或「有效数字」(默认/g/e),对字符串是「最多取几个字符」,对 % 还要先乘 100。f"{0.125:.1%}" 输出 12.5%——若你的业务值已经是 12.5(百分数值),直接用会得到 1250%,必须先除以 100 再格式化,或干脆不用 % 类型。
陷阱四:宽度按码点计,中文必然视觉错位
print(repr(f"{'博客':<10}"))
print(len('博客')) # 宽度按码点数计,2 个字符
'博客 '
2
规约的宽度数的是 Unicode 码点:'博客' 算 2 个字符,右补 8 个空格让总长正好 10。但终端里每个汉字占两列(东亚洲宽字符,East Asian Width),视觉上这一行就比纯英文的行多出两列——混排中文的报表「永远对不齐」。工程解法:要么报表只用英文/数字列,要么按显示宽度自行计算补空格(需要引入第三方宽度算法,不在本篇范围)。
陷阱五:截断先于补宽
f"{'hello world':10.5}" 得到 'hello' + 5 个空格,总长 10——先按精度截成 5 个字符,再按宽度补到 10。顺序反过来的直觉(先补到 10 再截)会得到 5 个字符,写错的人常对着输出发呆。
小结
本篇把格式规约的八段语法全部实测了一遍:四种对齐(</>/^/=)与任意填充字符;三种符号位(+/空格/-);# 进制前缀与 0 零填充(等价于 fill=0 + align==,对字符串只是普通填充);千分位 , 与 _;精度与类型(f/e/g/d/b/o/x/%,字符串精度等于截断);以及三个入口(f-string、str.format、format)共用同一套规约、嵌套字段可把宽度与对齐交给变量。结合第二章的 f-string 基础与第一章的 ljust/rjust/zfill 简单对齐,你的输出排版工具箱已经齐了。下一篇把这些语法用在真实场景:nginx 访问日志解析器的统计报表。
练习与思考题
- 写一个格式串,把
-1234.5678输出为-1,234.57(提示:分组、两位小数、符号位缺省即可),并解释每一段的含义。 - 已知
width与prec两个变量(如width=10, prec=3),输出3.14159265为右对齐、10 宽、3 位小数的字符串——要求用嵌套字段一次性写完。 - 用四种对齐 + 字符填充为
"PYTHON"生成一个居中的===PYTHON===装饰行(总宽 11),验证你的输出是否与预期一致。 - 思考题:报表中混排中文列为何对不齐?如果只能用一个纯标准库办法缓解(提示:全角转半角并不能解决显示宽度问题),你会怎么设计这个列?