0%

Python专题:字符串格式化

Python 有多种字符串格式化方法,主要包括:

  • 字符串格式设置运算符(%)
  • 模板字符串(Template)
  • str.format() 方法
  • f-string

%-formatting

% 称为“字符串格式设置运算符”。

% 运算符行为类似于 C 语言的 printf 函数(Java 中同样保留了该函数)。

在 Python 3.6 之后,官方推荐使用 f-string 或 str.format() 来代替 % 格式化。

  • 左侧操作数,是一个“格式字符串”,指定目标字符串的格式。
  • 右侧操作数,为格式字符串提示填充值。它可以是单值(如:字符串、数字等)、元组、字典。

格式字符串的完整定义形式是:%[( name )][ flags ][ width ].[ precision ] typecode

参数 必需? 说明
name 可选 数据值以字典类型提供时,指定字典的 key
flags 可选 见附录 flags 说明
width 可选 目标字符串占用宽度
precision 可选 数值型数据保留的小数位数
typecode 必需 数据值类型

【特殊说明】

1 flags

默认行为是:右对齐、正数不显示符号、用空格填充“空位”、八/十六进制不显示进制前缀。

2 precision

2.1 整数精度

精度作用于整数时,控制整数位数,不够补 0。(见【示例1-4】)
并且精度补的 0,不会被指定的空格前缀替换。(见【示例1-5】)

2.2 浮点数精度

对于浮点数而言,当不指定精度时,是否省略 . 的行为并不相同。
不省略时,相当于指定精度为 0。(见【示例1-6】)
省略时,相当于指定最高精度 6。(见【示例1-7】)

3 转义 %

% 字符是转换说明符的前缀,要原样输出,需要连写两次, 即 %%

4 限制

只支持 int/str/double 类型的格式化。

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
31
32
33
# 示例1-1:单值,字符串
format = "Hello, %s."
format % "world"
# 结果:'Hello, world.'

# 示例1-2:数字/元组
format = "Hi, %s %d..."
format % ("EZ", 666)
# 结果:'Hi, EZ 666...'

# 示例1-3:字典
format = '%(who)s is %(age)d'
values = {'who': 'EZ', 'age': 18}
format % values
# 结果:'EZ is 18'

# 示例1-4:整数精度,补 0
'%.4d' % 12
# 结果:'0012'
# 等价于 '%04d' % 12

# 示例1-5:整数精度,空格前缀不会替换精度补的 0。
# 注意,宽度 10 前有空格。
'% 10.4d' % 12
# 结果:' 0012'

# 示例1-6:浮点精度,不省略 .
'%.f' % 1.23456789
# 结果:'1'

# 示例1-7:浮点精度,省略 .
'%f' % 1.23456789
# 结果:'1.234568'

str.format()

str.format()Formateter 类共享相同格式字符串语法,与下文的 f-string 相似,但更简单,关键是不支持任意表达式。

格式化字符串包含的关键部分就是以花括号括起来的替换字段(replacement field),语法形式如下:

"{" [field_name] ["!" conversion] [":" format_spec] "}"

主要包括 3 部分:

  • 字段名(field name)
  • 转换标志(conversion)
  • 格式规格(format spec)

字段名

字段名不仅仅是个名称,可以是:

  • 索引序号
  • 关键字参数 Key
  • 属性访问/方法调用
  • 列表元素访问

字段名的值源自 format() 方法传入参数,传入参数可分为位置参数关键字参数,分别对应未命名字段命名字段

如果均传入位置参数,则未命名字段内容为参数传入的位置索引。由于格式字符串编号是显式指定的,也称“手动编号”。

1
2
"{0} {1} {2} {3} {0} {1}".format("to", "be", "or", "not")
# 'to be or not to be'

特殊地,如果未命名字段是按索引序使用的,则可以省略。没有显式指定编号,也称“自动编号”。

1
2
"{}, {}!".format("Hello", "world")
# 'Hello, world!'

如果以关键字参数传入,则命名字段内容为关键字参数的 Key。

1
2
"{hi}, {who}!".format(hi="hello", who="world")
# 'hello, world!'

未命名字段与命名字段是可以混用的,但是作为未命名字段不能混用自动编号与手动编号——换句话说,只能全部使用自动编号或手动编号。

1
2
3
4
# 自动编号字段与关键字字段共用
"{foo} {} {bar} {}".format(2, 4, bar=3, foo=1)
# 手动编号字段与关键字字段共用
"{foo} {1} {bar} {0}".format(4, 2, bar=3, foo=1)

转换标志

转换标志,用于在格式化前将值转换为一个字符串。共有三种,!s!r!a 分别调用 str()repr()ascii() 转换。

1
2
"{pi!s} {pi!r} {pi!a}".format(pi="π")
结果:"π 'π' '\\u03c0'"

格式规格

描述值的字段宽度、对齐方式、填充符号、小数精度等呈现细节。它由一种“格式规格迷你语言”编写。其语法形式为:

[[fill]align][sign][#][0][width][grouping_option][.precision][type]

一共分为八个部分:

  • fill:填充字符,任意字符。默认为空格。需要后跟有效align
  • align:对齐方式。候选值包括:< > = ^。(详见“附录”)

  • sign:符号位。候选值包括:+ - “ “(空格)。(详见“附录”)

  • #替代形式转换。仅适用于数值型(整数、浮点数、复数),不同类型。(详见“附录”)
  • 00 填充。即 fillalign 未指定时,将在符号与数字点填充 0。等价于 fill 为 0、align=
  • width:最小字段宽度。
  • grouping_option:分组分隔符。候选值包括:_ ,。(详见“附录”)
  • precision:精度。
  • type:数据类型。
1
2
3
4
# 格式说明符
from math import pi
"π = {name:*^+#020_.2f}".format(name=pi)
# 结果:'π = *******+3.14********'

f-string

f-string 是 Python 3.6 新引入的字符串字面值,是一种格式化字符串字面值,PEP 术语也称为“字符串插值”(Literal String Interpolation)。

它以 f 或 F 为前缀——代表“formatted string”——需运行时计算的表达式置于一对花括号中。形式如下:

1
f ' <text> { <expression> <optional !s, !r, or !a> <optional : format specifier> } <text> ... '

!s!r!a 3 个称为“转换标志符”,它指明用什么“方法”转换。
: 后的部分称为“格式说明符(Format Specifier)”,用于指定如何格式化变量的值。
更多示例说明请参考 Python:f-string 格式化对象

由于不存在显式传入参数,所以表达式不可能使用位置索引,只能使用当前作用域中的变量——f 字符串原理上就是转换为 format(),因此理论上两者的格式字符串大部分是互通的——即使如此,f-string 的语法也比 format() 的格式字符串语法复杂。

【特殊说明】

1 转义花括号

由于花括号是定界符,所以需要转义——使用双花括号 {{}}

2 格式表达式不允许的字符

#\ 会导致 SyntaxError。要使用包含这些字符的值,可以创建临时变量来存储。如:newline = '\n' 则可用 f'newline: {ord(newline)}'

3 不能用作文档字符串

即使不包含表达式 f 字符串也不能用作文档字符串。

4 调试模式

表达式后加等号,会输出表达式文本、=、求值结果,这通常用于调试打印内容。(见【示例4-3】)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 示例4-1:
name = "EZ"
f"My name is {name!r}."
f"My name is {repr(name)}." # repr() <=> !r

# 示例4-2:数字格式化
import decimal
price = decimal.Decimal('42.123456')
width = 6
precision = 4
f"result: {price:{width}.{precision}}" # 嵌套字段
# 结果:'result: 42.12'

# 示例4-3:日期格式化
from datetime import datetime
today = datetime(year=2022, month=8, day=12)
f"{today:%B %d, %Y}"
# 结果:'August 12, 2022'
f"{today=:%B %d, %Y}" # debug
# 结果:'today=August 12, 2022'

模板字符串

模板字符串使用类 UNIX shell 语法。

1
2
3
4
from string import Template
tmpl = Template("Hello, $who!")
tmpl.substitute(who="World")
# 'Hello, World!'

模板字符串旨在简化基本的格式设置机制,因此,它的用法相对而言更简单。(详见“模板字符串”章节)

模板字符串支持基于 $ 的替换,替换占位符格式为 $identifier——因此命名应符合标识符规则——注意,不区分大小写。

【特殊说明】

1 另一种形式

默认情况下, 替换占位符会从 $ 一直延续到第一个非标识符字符终结。因此如果其后紧跟合法标识符字符,则需要使用 ${identifier} 形式。

2 转义 $

$ 是替换占位符的前缀,要原样输出。应使用 $$ 转义。

3 限制

无法控制指定对象到字符串的转化。

Template 类提供了两个方法将模板字符串替换为目标字符串:

1
2
substitute(mapping={}, /, **kwds)
safe_substitute(mapping={}, /, **kwds)

可以使用字典或一组关键字参数传入“替换键值对”,如果同时使用,则以关键字参数组优先。

substitute vs. safe_substitute

当存在未传入的替换占位符时,substitute 方法会抛出 KeyError;非法位置出现 $substitute 方法会抛出 ValueError,而 safe_substitute 方法静默地原样输出。

【扩展说明】

如果你熟悉 PowerShell 的相关语法,模板字符串将很容易理解。

小结

从代码角度来说,除 f-string 外三个方法的一个缺点是:插入的值来源于“下文”而非“上文”。换句话说,当读到格式字符串时,我们并不能马上知道格式化后的字符串到底是什么内容,还需要继续阅读代码。f-string 则没有这个问题,其插值在模板出现之前就已经给出了。另外,f-string 速度更快。

  • % 格式字符串语法

    %[( name )][ flags ][ width ].[ precision ] typecode

  • format() 替换字段语法

    "{" [field_name] ["!" conversion] [":" format_spec] "}"

  • format() 格式规格语法

    [[fill]align][sign][#][0][width][grouping_option][.precision][type]

  • f-string 语法

    f ' <text> { <expression> <optional !s, !r, or !a> <optional : format specifier> } <text> ... '

扩展示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 示例1:% 防御性编程
msg = ('World', 42)
'Hello, %s' % msg
# 执行异常,错误信息如下:
# TypeError: not all arguments converted during string formatting
# 为防止参数类型错误,可以如下编写代码
# 注意参数的类型转换,这里元组参数会先字符串化
'Hello, %s' % (msg, )
# 结果:'Hello, ('World', 42)'

# 示例2:str.format 表达式与 f-string 的区别
d = {'a': 10, 'b': 20}
'a={d[a]}'.format(d=d) # 字典 key 不要引号
f'a={d["a"]}' # 字典 key 需要引号
# 结果:'a=10'
a = 'b'
f'a={d[a]}'
# 结果:'a=20'
# key 不必是合法的 Python 表达式
'{i[+-*/]}'.format(i={'+-*/':42})
# 结果:'42'

附录

% 运算符

flags 符号

符号 功能
- 左对齐
+ 正数前缀显示加号(+
空格 正数前缀显示空格(代表符号位)
# 八进制前缀显示 0o
十六进制前缀显示 0x/0X
0 右对齐时,“空位”填充 0(而非空格,左对齐无效果)

typecode 符号

符号 描述
s 字符串
d 整数
f 浮点数(可指定小数位数)
e 浮点数科学计数法
E 作用同 %e(大写 E)
g %f 和 %e 的简写
G %f 和 %E 的简写
u 无符号整型
c 字符及其 ASCII 码
o 无符号八进制数
x 无符号十六进制数
X 无符号十六进制数(大写 X)

转换标志

转换符 调用方法
!s str()
!r repr()
!a ascii()

注:f-string 和 str.format() 都支持。

格式规格迷你语言

对齐选项(align)

选项 含意
< 左对齐(大多数对象的默认值)
> 右对齐(数字的默认值)
= 在符号与数字间填充(0 在字段宽度前的默认值)
^ 居中对齐

注:只有最小字段宽度大于字段宽度时,填充与对齐才有意义。

符号位(sign)

选项 含意
+ 正数显示前缀 +,负数显示前缀 -
- 仅负数显示前缀 -(默认行为)
space 正数显示前缀空格,负数显示前缀 -

注:仅对数字类型有效。

替代形式(#)

类型 含意
二进制整数 添加前缀 0b
八进制整数 添加前缀 0o
十六进制整数 添加前缀 0x0X
浮点/复数 总包含小数点

分隔符(grouping option)

选项 适用类型 版本 含意
, 十进制整数、浮点数、复数 3.1+ 千位分隔符。需要随区域改变,类型应使用 n
_ 十进制整数、浮点数,二进制、八进制、十六进制整数 3.6+ 十进制整数、浮点数时用作千位分隔符,二进制、八进制、十六进制整数时每四位插入一个分隔符。

精度(precision)

作用类型 含意
f F 小数点后显示的数位
g G 小数点前后总共显示的数位
s 最大字段宽度

类型符号

符号 值类型 含意
s 字符串 字符串,字符串默认
b 整数 二进制整数
c 整数 字符,整数将转换为 unicode 字符
d 整数 十进制整数
o 整数 八进制整数
x 整数 十六进制整数
X 整数 十六进制整数,前缀大写 x,字母数码大写
n integer/float/Decimal d / g,分隔字符随当前区域不同
e float/Decimal 科学计数法
E float/Decimal 类似 e,大写 E 分隔字符
f float/Decimal 定点表示法
F float/Decimal 类似 fnan => NANinf => INF
g float/Decimal 常规格式
G float/Decimal 类似 ge 分隔字符、naninf 等大写
% float/Decimal 百分比,数值乘以 100 按 f 格式显示并加百分号

参考

Python doc - 内置函数 format

Python doc - 常见字符串操作:格式字符串语法、模板字符串等说明。

Python doc - 格式字符串字面值:提供了 f 字符串的语法定义。

PEP 498 – Literal String Interpolation:f 字符串提案。讨论部分介绍了 f-string 与 str.format 表达式的区别、表达式中的 lambda等问题。

Python doc - datetime:提供了日期时间格式化代码。

后记

本来只是想归纳下字符串格式化的各种方法,一一列举后发现常用的也就只有 4 种,便满心欢喜的以为只需要写一篇短文就可以搞定了。

以为自己大部分知识点都已清楚了,但是,稍一深入就感觉有点不对了,没想到盲点还不少。在 flomo 回顾笔记的时候才发现,这就是被称为“解释性深度错觉”的心理偏误——人们以为自己知道的往往是不知道的。正应了那句话:要想知道自己有多无知,就选一个话题一直说下去。

因此,也深深体会到写技术文章的好处,它可以让你知道什么东西自己不知道,而借此正好去知道。