字符串格式化进阶(f-string / format / %)

前面 字符串基础与常用方法输入输出与格式化字符串 都提过 f-string,但那只是入门。字符串格式化是 Python 里最需要”知道更多”的话题之一——你以为你会写 f"{x:.2f}",但一遇到”数字千分位加两位小数”、”日期时间”、”补零对齐”、”表达式调试”,就抓瞎了。这一篇把三种格式化姿势(f-string、str.format、%)的语法一次讲透,重点是 format-spec 迷你语言——这才是格式化的”内核”。

一、三代格式化

新代码请用 f-string,其他两代只用于阅读老代码:

1
2
3
4
5
6
7
8
9
10
name, age = "咖飞", 3

# %(1990 年代)
"我叫 %s,%d 岁" % (name, age)

# str.format()(Python 2.6+)
"我叫 {},{} 岁".format(name, age)

# f-string(Python 3.6+,推荐)
f"我叫 {name}{age} 岁"

三者共享的”格式规范”

无论哪种方式,冒号后面的部分(format spec) 都是同一套迷你语言:

1
2
3
"{:.2f}".format(3.14159)       # '3.14'
f"{3.14159:.2f}" # '3.14'
"%.2f" % 3.14159 # '3.14'

学好 format spec 是关键。

二、format spec 完整语法

格式规范的完整语法(简化版):

1
[[fill]align][sign][#][0][width][,_][.precision][type]
  • fill:填充字符(默认为空格);
  • align:对齐 <(左)、>(右,数字默认)、^(居中)、=(数字符号后填充);
  • sign+(正数显示 +)、-(默认,只显示负号)、 (正数留空格);
  • #:显示进制前缀(0x、0b、0o);
  • 0:数字用 0 填充;
  • width:最小总宽度;
  • , 或 _:千分位分隔符;
  • .precision:小数位数或字符串截断长度;
  • typed/b/o/x/X(进制)、f/e/g(浮点)、s(字符串)、%(百分比)。

三、宽度与对齐

1
2
3
4
5
6
f"{42:5d}"       # '   42'      默认右对齐
f"{42:<5d}" # '42 '
f"{42:^5d}" # ' 42 '
f"{42:*^5d}" # '**42*' 用 * 填充
f"{'py':>10}" # ' py'
f"{'py':-^10}" # '----py----'

四、补零

1
2
3
f"{7:04d}"       # '0007'   4 位宽度,前补 0
f"{-7:04d}" # '-007' 带负号也占位
f"{7:=04d}" # '0007' 显式指定"=对齐"(数字默认)

五、小数位与千分位

1
2
3
4
5
6
7
f"{3.14159:.2f}"       # '3.14'
f"{3.14159:.5f}" # '3.14159'
f"{3:.2f}" # '3.00' 整数也能格式化成 .2f

f"{1234567:,}" # '1,234,567'
f"{1234567.89:,.2f}" # '1,234,567.89'
f"{1234567:_}" # '1_234_567' Python 3.6+ 下划线分隔

六、百分比

1
2
3
f"{0.256:.1%}"    # '25.6%'
f"{0.256:.0%}" # '26%'
f"{-0.05:+.1%}" # '-5.0%' 显式加号

% 会自动 *100 并加百分号,非常方便。

七、进制

1
2
3
4
5
6
f"{255:b}"       # '11111111'
f"{255:o}" # '377'
f"{255:x}" # 'ff'
f"{255:X}" # 'FF'
f"{255:#x}" # '0xff'
f"{255:#010b}" # '0b11111111' # 表示带前缀,010 表示 10 位宽补零

八、正负号

1
2
3
f"{3:+}"         # '+3'
f"{-3:+}" # '-3'
f"{3: }" # ' 3' 正数留空格

九、字符串格式化

1
2
3
f"{'py':10}"     # 'py        '  默认左对齐
f"{'py':>10}" # ' py' 右对齐(字符串默认左)
f"{'long text':.4}" # 'long' 截断到 4 字符

十、日期时间

datetimedatetime 都支持 strftime 格式:

1
2
3
4
5
6
7
from datetime import datetime, date

now = datetime.now()
f"{now:%Y-%m-%d %H:%M:%S}" # '2025-09-10 22:03:19'
f"{now:%Y年%m月%d日}" # '2025年09月10日'
f"{now:%A}" # 'Wednesday'
f"{date.today():%Y/%m/%d}" # '2025/09/10'

十一、动态宽度和精度

宽度、精度可以是变量:

1
2
3
4
5
w = 10
p = 3

f"{3.14159:{w}.{p}f}" # ' 3.142'
f"{3.14159:{'>'}{w}.{p}f}" # 同上,动态对齐

十二、= 调试语法(Python 3.8+)

f-string 的”表达式调试神器”:

1
2
3
4
5
6
7
x, y = 42, 10
print(f"{x=}") # x=42
print(f"{x+y=}") # x+y=52
print(f"{x*y=:.2f}") # x*y=420.00

user = {"name": "咖飞", "age": 3}
print(f"{user['name']=}") # user['name']='咖飞'

以前需要 print("x =", x),现在 f-string 直接搞定,是最实用的现代 Python 语法之一。

十三、r 和 !r 强制 repr

1
2
3
4
5
6
7
8
name = "咖飞"
print(f"{name}") # 咖飞
print(f"{name!r}") # '咖飞' 带引号,输出 repr
print(f"{name!s}") # 咖飞 默认,等价 str
print(f"{name!a}") # '咖飞' ascii-only

# 在调试语法里
print(f"{name=!r}") # name='咖飞'

!r 在打印/日志里非常有用——能明显区分字符串和数字。

十四、嵌套 f-string

f-string 里可以嵌套 f-string,但要小心引号冲突:

1
2
3
4
name = "咖飞"
# Python 3.11 前,引号不能跟外层一样
f'{f"内层 {name}":>10}'
# Python 3.12+ 允许同类型引号嵌套

十五、str.format 的独有能力

format 版本在从外部读格式模板时优于 f-string(因为 f-string 是编译期解析):

1
2
3
4
5
6
7
8
9
10
11
12
template = "{name} 得了 {score} 分"
config_format = template.format(name="咖飞", score=99)

# 命名参数
"{last} {first}".format(first="咖", last="飞")

# 索引参数(可以重复使用)
"{0} {1} {0}".format("a", "b")

# 属性/索引访问
"{p.name}".format(p=some_obj)
"{d[key]}".format(d={"key": "value"})

f-string 里也能做属性和索引访问,只是不能”延迟求值”。

十六、% 老式格式化

只在维护老代码时会用到:

1
2
3
"%s is %d years old" % ("咖飞", 3)
"%-10s | %5.2f" % ("cpu", 3.14)
"%(name)s %(age)d" % {"name": "咖飞", "age": 3}

新代码请一律用 f-string

十七、Locale 与国际化

想要”按当前地区规则显示数字”(比如欧洲用 . 做千分位):

1
2
3
4
5
import locale
locale.setlocale(locale.LC_ALL, "")

# n 格式会用 locale 规则
f"{1234567.89:n}" # 中文环境:'1234567.89'

标准的 , 千分位是”美式”,locale 提供跨地区支持。

十八、一个综合例子:账单打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# invoice.py
"""格式化打印一份账单,展示各种 format spec 用法。"""

from datetime import datetime

items = [
("苹果", 10, 3.50),
("面包", 2, 8.00),
("咖啡豆", 1, 128.00),
("巧克力", 5, 15.90),
]

# 表头
print(f"{'咖飞小店':^40}")
print(f"{'账单':^40}")
print(f"{datetime.now():%Y-%m-%d %H:%M:%S}")
print("-" * 40)
print(f"{'商品':<10}{'数量':>6}{'单价':>10}{'小计':>12}")
print("-" * 40)

total = 0
for name, qty, price in items:
subtotal = qty * price
total += subtotal
print(f"{name:<10}{qty:>6d}{price:>10,.2f}{subtotal:>12,.2f}")

print("-" * 40)
print(f"{'合计':<10}{'':>6}{'':>10}{total:>12,.2f}")
print(f"{'税率':<10}{'':>6}{'':>10}{0.13:>12.1%}")
print(f"{'含税':<10}{'':>6}{'':>10}{total * 1.13:>12,.2f}")

输出效果整齐对齐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
                  咖飞小店
账单
2025-09-10 22:03:19
----------------------------------------
商品 数量 单价 小计
----------------------------------------
苹果 10 3.50 35.00
面包 2 8.00 16.00
咖啡豆 1 128.00 128.00
巧克力 5 15.90 79.50
----------------------------------------
合计 258.50
税率 13.0%
含税 292.11

十九、常见陷阱

  1. f-string 内的引号冲突(3.11 之前):f"{d["key"]}" → 报错。改成 d['key']
  2. 忘了 f 前缀"{name}" 只是字面串,不会替换。
  3. {} 字面:想输出 {,写 {{`;`}}
  4. 格式化 Nonef"{None:.2f}" 会报错,先判空或转字符串。
  5. %s 后面变量个数不匹配"%s %s" % ("a",) 会 TypeError。f-string 没这个问题。
  6. str.format(**d) 里 key 是数字或有非法字符:得用位置或 format_map
  7. 精度对整数无效f"{100:.2f}" 会输出 100.00(自动转 float),但 f"{100:.2d}" 报错——d 是整数格式,不接受精度。

二十、小结与延伸阅读

  • 新代码永远用 f-string;模板从外部读时用 str.format()
  • 冒号后面是 format spec 迷你语言:宽度、对齐、填充、千分位、精度、进制;
  • % 系列被 f-string 全面取代,只出现在老代码;
  • = 调试语法、!r 强制 repr、日期时间 %Y-%m-%d 都要掌握;
  • Python 3.12 前 f-string 内引号有限制;
  • 千分位用 ,,百分比用 .1%,日期用 strftime 语法。

延伸阅读:

下一篇 序列切片深入解析 我们要把 Python 切片的门道彻底讲清。