字符串格式化是指把值转换成字符串,再插入到字符串模板中的过程。 Python 之禅 中说,在 Python 里做一件事应该只有一种 显而易见 的方法。 但一说到字符串格式化,事情就有点……不那么禅了。
令人意外的是,在 Python 里进行字符串格式化主要有 4 种方式,各自面向不同的场景。
这其中一部分源于 Python 悠久的历史,另一部分则出于国际化或输入清洗这类考虑。
我们会从字符串格式化工具箱里最新加入的方式讲起,再一路往回,讲到“旧式”或“printf()风格”的字符串格式化。
f-string
Python 3.6 引入了 f-string(“formatted strings”的简称),也叫字面量字符串插值。在引号前加上 f(或 F)前缀,就能快速高效地把表达式和字符串求值(并格式化)为 str 类型(写法如下:f'{object}')。
f-string 可以用于所有类型的包围引号,例如单行('或")和多行('''或""")。
放进{}里的任何变量、表达式或其他类型,都会先被求值,再转换成 str,最后与花括号外的所有 str 拼接起来。
在下面这个例子里,我们把两个变量的值插入到一句话中(一个str和一个float):
>>> name = 'eighth'
>>> value = 1/8
# The f-string, using the two values.
# The .2f format code truncates, so the value displays as 0.12.
>>> f'An {name} is approximately {value:.2f}.'
'An eighth is approximately 0.12.'
可以求值的表达式几乎无所不包。
可求值的可能性很多,例如:str、数字、变量、算术表达式、条件表达式、内置类型、切片、函数、lambda、推导式,或者任何定义了 __str__ 或 __repr__ 方法的对象。
从简单到复杂,依次来看:
插入变量,这是f-string最简单的用法,就是把变量直接放进字符串里。
# Assigning a variable.
>>> name = "World"
# Inserting that variable.
>>> f'Hello, {name}!'
'Hello, World!'
{}里的表达式,花括号内可以求值任何合法的 Python 表达式。
注意,在单引号f-string里使用双引号(反过来也一样)可以避免使用转义序列:
# A dictionary of key:value pairs.
>>> waves = {'water': 1, 'light': 3, 'sound': 5}
# Inserting the whole dict.
>>> f'Wave ranks: {waves}'
"Wave ranks: {'water': 1, 'light': 3, 'sound': 5}"
# An expression can be evaluated inline:
>>> f"Tenfold the value of 'light' is {waves['light'] * 10}."
"Tenfold the value of 'light' is 30."
# A method call can also be evaluated inline:
>>> f'{"hello world!".title()} is a classic greeting.'
'Hello World! is a classic greeting.'
# An f-string can be nested inside another f-string:
>>> f"{f'hello world!'.title()} is a classic greeting."
'Hello World! is a classic greeting.'
输出格式化,可以用格式说明迷你语言来控制对齐、数字精度等等。
格式说明写在值之后,中间用:分隔。
# Right-align a value to ten characters, and round it to 3 decimal places.
>>> value = 1 / 7
>>> f'One seventh is {value:10.3f}.'
'One seventh is 0.143.'
# A format specification can be set using variables as well.
>>> padding = 10
>>> precision = 3
>>> f'One seventh is {value:{padding}.{precision}f}.'
'One seventh is 0.143.'
综合运用,把变量、表达式、函数调用和输出格式化结合起来:
>>> precision = 3
>>> f"{30e8 * 111_000:6.{precision}e}"
'3.330e+14'
>>> verb = 'meet'
>>> the_end = ['end', 'of', 'transmission']
>>> f'"Have a {"NICE".lower()} day, I will {verb} you after {30e8 * 111_000:6.{precision}e} light-years."{the_end}'
'"Have a nice day, I will meet you after 3.330e+14 light-years."[\'end\', \'of\', \'transmission\']'
# Did you notice the escaped single-quotes in the previous example?
# Using double quotes instead of single quotes for the f-string means the list's single-quoted strings print cleanly.
>>> f"Have a nice day. {the_end}"
"Have a nice day. ['end', 'of', 'transmission']"
有两个主要限制需要注意。
f-string 表达式不能为空。
另外,在 Python 3.12 之前,它们不能包含注释。
>>> f"An empty expression will error: {}"
SyntaxError: f-string: empty expression not allowed
>>> word = 'word'
>>> f"""A comment in a triple quoted f-string: {
word # I chose a nice variable
}"""
'A comment in a triple quoted f-string: word'
字符串插值无法与用于国际化(I18N)和本地化(L10N)的 GNU gettext API 一起使用,因此在任何“字符串包装”的翻译场景中,建议用下面介绍的 string.Template 类或 str.format() 方法代替f-string。
另外还要记住,在f-string的括号{}里使用表达式,与使用 eval() 或 exec() 类似,所以并不十分安全,绝不能用于用户输入。
str.format() 方法str.format() 方法会用作为实参传入的值替换字符串中的占位符。
占位符可以用名称({price})、数字({0} 或索引)标识,甚至可以是空的(位置)占位符{}。
例如:
# A named placeholder and a positional placeholder.
>>> 'My text: {placeholder_1} and {}.'.format(12, placeholder_1='named placeholder')
'My text: named placeholder and 12.'
和f-string一样,Python 的 str.format() 也支持一整套迷你语言格式说明符,可以用来对齐文本、截断浮点数等等。
完整的格式说明符模式是 {[<name>][!<conversion>][:<format_specifier>]}:
<name> 可以是具名占位符、数字,或者留空。!<conversion> 是可选的,取值应为以下三种转换之一:!s 对应 str(),!r 对应 repr(),!a 对应 ascii()。
默认使用 str()。:<format_specifier> 是可选的,用来控制值的显示方式。
关于可用选项的更多信息可以在这里找到。下面是一个带变音符号字母的转换示例:
# Fills in the object at index zero, converted to a string.
>>> 'An e with an umlaut: {0!s}'.format('ë')
'An e with an umlaut: ë'
# Fills in the object at index zero, converted to a repr.
>>> 'An e with an umlaut (object representation): {0!r}'.format('ë')
"An e with an umlaut (object representation): 'ë'"
# Fills in the object at index zero, converted to ASCII.
>>> 'An e with an umlaut converted into ASCII: {0!a}'.format('ë')
"An e with an umlaut converted into ASCII: '\xeb'"
# Fills in the object in the first position,
# then fills in the object in the second position formatted as a repr.
>>> 'She said her name is not {} but {!r}.'.format('Chloe', 'Zoë')
"She said her name is not Chloe but 'Zoë'."
常见格式说明符的示例:
# Integer and binary/hex representations of the same number.
>>> my_num = 42
>>> f"{my_num} in binary is {my_num:b}. In hex, it is {my_num:x}"
"42 in binary is 101010. In hex, it is 2a"
# Alignment: left (<), right (>), and center (^) using up to ten characters total.
>>> f"[{"left":<10}] [{"right":>10}] [{"center":^10}]"
"[left ] [ right] [ center ]"
# Float precision and scientific notation (up to three decimal places).
>>> pi = 3.141592653589793
>>> f"Fixed: {pi:.3} Scientific: {pi:.3e}"
"Fixed: 3.142 Scientific: 3.142e+00"
# Thousands separator and percentage.
>>> balance = 1000
>>> rate = 0.0225
>>> f"Balance: ${balance:,.0f} Interest rate: {rate:.1%}"
"Balance: $1,000 Interest rate: 2.2%"
# Putting it all together:
>>> items = [("Widget", 1250, 9.991), ("Gadget", 37, 24.503), ("Doohickey", 4, 149.002)]
>>> header = f"{"Item":<12} {"Qty":>6} {"Price":>9}"
>>> print(header)
Item Qty Price
>>> for name, qty, price in items:
... print(f"{name:<12} {qty:>6} {price:>9.2f}")
Widget 1250 9.99
Gadget 37 24.50
Doohickey 4 149.00
更多示例见这份文档末尾。
% 格式化,或 printf() 风格格式化用%运算符进行格式化,是 Python 中最古老的字符串格式化方式。
它源自 C 语言,可以用位置实参来构建str。
这种方式已经被f-string和 str.format() 取代,所以%格式化又被称为_“旧式”。
在 Python 2 或遗留代码中仍然能看到它。
虽然这种方式在 Python 3.x 中仍然可用,但通常都会避免使用%格式化,因为它容易出错、效率较低、可用选项更少,而且占位符与实参不匹配时会抛出异常。
使用%运算符的方式与 printf() 类似,所以有时也被称为printf 格式化_。
# Assigning a variable.
>>> name = "Anna-conda"
# Building a string using %.
>>> "The snake's name is %s." % name
"The snake's name is Anna-conda."
在上面的例子里,% 运算符在运行时用变量 name 替换占位符 %s。
如果想在字符串里加入多个变量,就需要在%后面提供一个元组,每个占位符对应元组中的一个对象:
# Assigning variables.
>>> name = "Billy the Kid"
>>> fruit = "grapes"
# Building a string using %.
>>> "Surprisingly, %s's favorite snack was %s." %(name, fruit)
"Surprisingly, Billy the Kid's favorite snack was grapes."
string.Template()(不要和 Python 3.14 的 t-strings 混淆)是 string 模块里的一个类(不同于内置的 str 类型)。它属于 Python 标准库,但使用时需要先导入。
模板字符串支持基于$的替换,比这里提到的其他方式都更简单,功能也更弱。
不过,在处理复杂的国际化或清洗外部输入时,它能派上大用场。
对于不受信任的用户输入,string.Template 被认为更安全,因为它不会求值任意表达式,也不会访问对象属性,从而缓解格式字符串注入攻击。
>>> from string import Template
>>> name = "Anna-Conda"
# Creating a Template with placeholder text.
>>> template_string = Template("The snake called `$snake_name` has escaped!")
# Calling .substitute() to replace the placeholder with a value.
>>> template_string.substitute(snake_name=name)
'The snake called `Anna-Conda` has escaped!'
关于 string.Template 的更多信息,可以查阅 Python 的文档。
面对这么多选项和迷你语言,格式化 Python 字符串时到底该用哪一个呢? 这里有几条简单的准则:
f-string 最新,也最易读。
如果你的代码不需要国际化,那么在 Python 3.6 及以上版本中,它应当作为首选。
str.format() 用途广泛、功能强大,既能配合 gnu gettext,也兼容大多数 Python 版本。
如果你需要的是简单、安全,或者大量的国际化处理,那么 string.Template() 可以在处理用户输入时降低风险,也适合包装翻译字符串。
对于新代码,% 运算符通常被认为已过时,尽管它在现代 Python 中仍然可用。
它主要用于兼容旧的代码库。
相比其他方式,% 格式化更容易出错(功能也更少),还可能在显示非 ASCII 字符时出问题。
如果你打算使用它,请查阅你所用的 Python 发行版,了解具体的支持情况。
如果你想深入了解,all about formatting 和 Python String Formatting Best Practices 都是不错的起点。