PEP 498 – 字面量字符串插值
- 作者:
- Eric V. Smith <eric at trueblade.com>
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2015年8月1日
- Python 版本:
- 3.6
- 发布历史:
- 2015年8月7日,2015年8月30日,2015年9月4日,2015年9月19日,2016年11月6日
- 决议:
- Python-Dev 消息
摘要
Python 支持多种方式来格式化文本字符串。其中包括 %-格式化 [1]、str.format()
[2] 和 string.Template
[3]。这些方法各有其优点,但也有缺点,使得它们在实践中使用起来很麻烦。本 PEP 提议增加一种新的字符串格式化机制:字面量字符串插值。在本 PEP 中,此类字符串将被称为“f-string”,取自用于表示此类字符串的开头字符,代表“格式化字符串”。
本 PEP 不提议删除或废弃任何现有的字符串格式化机制。
f-string 提供了一种以最少的语法将表达式嵌入到字符串字面量中的方法。需要注意的是,f-string 实际上是在运行时求值的表达式,而不是常量值。在 Python 源代码中,f-string 是一个以“f”为前缀的字面量字符串,其中包含大括号内的表达式。表达式将被替换为它们的值。一些示例如下:
>>> import datetime
>>> name = 'Fred'
>>> age = 50
>>> anniversary = datetime.date(1991, 10, 12)
>>> f'My name is {name}, my age next year is {age+1}, my anniversary is {anniversary:%A, %B %d, %Y}.'
'My name is Fred, my age next year is 51, my anniversary is Saturday, October 12, 1991.'
>>> f'He said his name is {name!r}.'
"He said his name is 'Fred'."
PEP 215 中曾提出过类似的功能。PEP 215 提议支持 Python 表达式的一个子集,并且不支持 PEP 3101 引入的类型特定字符串格式化(__format__()
方法)。
基本原理
本 PEP 的驱动力是希望在 Python 中有一种更简单的方式来格式化字符串。现有的格式化方式要么容易出错,要么不灵活,要么繁琐。
%-格式化在它支持的类型方面是有限的。只有 int、str 和 double 可以被格式化。所有其他类型要么不受支持,要么在格式化之前转换为这些类型之一。此外,当传递单个值时,存在一个众所周知的陷阱:
>>> msg = 'disk failure'
>>> 'error: %s' % msg
'error: disk failure'
但如果 msg 曾经是元组,相同的代码就会失败:
>>> msg = ('disk failure', 32)
>>> 'error: %s' % msg
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: not all arguments converted during string formatting
为了安全起见,应使用以下代码:
>>> 'error: %s' % (msg,)
"error: ('disk failure', 32)"
添加 str.format()
是为了解决 %-格式化的一些问题。特别是,它使用正常的函数调用语法(因此支持多个参数),并且可以通过被转换为字符串的对象的 __format__()
方法进行扩展。有关详细原理,请参阅 PEP 3101。本 PEP 重用了 str.format()
的大部分语法和机制,以与现有 Python 字符串格式化机制保持连续性。
然而,str.format()
也并非没有问题。其中最主要的问题是它的冗长。例如,文本 value
在这里重复出现:
>>> value = 4 * 20
>>> 'The value is {value}.'.format(value=value)
'The value is 80.'
即使在最简单的形式中,也有一些样板代码,并且插入到占位符中的值有时与占位符所在的位置相距甚远:
>>> 'The value is {}.'.format(value)
'The value is 80.'
使用 f-string,这会变成:
>>> f'The value is {value}.'
'The value is 80.'
f-string 提供了一种简洁、易读的方式来在字符串中包含 Python 表达式的值。
从这个意义上说,string.Template
和 %-格式化与 str.format()
有类似的缺点,而且支持的格式化选项更少。特别是,它们不支持 __format__
协议,因此无法控制特定对象如何转换为字符串,也不能扩展到其他需要控制如何转换为字符串的类型(例如 Decimal
和 datetime
)。这个例子在使用 string.Template
时是不可能的:
>>> value = 1234
>>> f'input={value:#06x}'
'input=0x04d2'
%-格式化和 string.Template
都无法控制诸如以下格式:
>>> date = datetime.date(1991, 10, 12)
>>> f'{date} was on a {date:%A}'
'1991-10-12 was on a Saturday'
不使用 globals() 或 locals()
在 python-dev 上的讨论中 [4],提出了许多使用 locals() 和 globals() 或其等价物的解决方案。所有这些都存在各种问题。其中包括引用闭包中未使用的变量。考虑:
>>> def outer(x):
... def inner():
... return 'x={x}'.format_map(locals())
... return inner
...
>>> outer(42)()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in inner
KeyError: 'x'
这会返回一个错误,因为编译器没有在闭包内添加对 x 的引用。你需要手动添加对 x 的引用才能使其工作:
>>> def outer(x):
... def inner():
... x
... return 'x={x}'.format_map(locals())
... return inner
...
>>> outer(42)()
'x=42'
此外,使用 locals() 或 globals() 会引入信息泄漏。一个可以访问调用者的 locals() 或 globals() 的被调用例程,比执行字符串插值所需的信息访问了更多。
Guido 声明 [5],任何更好的字符串插值解决方案都不会在其实现中使用 locals() 或 globals()。(这不禁止用户传入 locals() 或 globals(),只是不要求它,也不允许在底层使用这些函数。)
规范
在源代码中,f-string 是以字母“f”或“F”为前缀的字符串字面量。本 PEP 凡是使用“f”的地方,也可以使用“F”。“f”可以与“r”或“R”以任意顺序结合,以生成原始 f-string 字面量。“f”不能与“b”结合:本 PEP 不提议添加二进制 f-string。“f”不能与“u”结合。
在对源文件进行标记化时,f-string 使用与普通字符串、原始字符串、二进制字符串和三引号字符串相同的规则。也就是说,字符串必须以其开始的相同字符结束:如果以单引号开始,则必须以单引号结束,等等。这意味着任何当前扫描 Python 代码以查找字符串的代码都应该可以很容易地修改以识别 f-string(当然,在 f-string 内进行解析是另一回事)。
标记化后,f-string 被解析为字面量字符串和表达式。表达式出现在大括号 '{'
和 '}'
之间。在扫描字符串以查找表达式时,f-string 的字面量部分内的任何双大括号 '{{'
或 '}'
是一个错误:字面量闭大括号必须双写 '}}'
才能表示单个闭大括号。
f-string 中大括号之外的部分是字面量字符串。这些字面量部分随后被解码。对于非原始 f-string,这包括将反斜杠转义序列(例如 '\n'
、'\"'
、"\'"
、'\xhh'
、'\uxxxx'
、'\Uxxxxxxxx'
和命名的 Unicode 字符 '\N{name}'
)转换为其关联的 Unicode 字符 [6]。
反斜杠不得出现在表达式的任何位置。表达式内不允许使用 '#'
字符表示的注释。
在每个表达式之后,可以指定一个可选的类型转换。允许的转换是 '!s'
、'!r'
或 '!a'
。它们与 str.format()
中的处理方式相同:'!s'
对表达式调用 str()
,'!r'
对表达式调用 repr()
,'!a'
对表达式调用 ascii()
。这些转换在调用 format()
之前应用。使用 '!s'
的唯一原因是如果你想指定一个适用于 str
而不是表达式类型的格式说明符。
F-string 使用与 str.format
相同的格式说明符微语言。与 str.format()
类似,可选的格式说明符可以包含在 f-string 内部,与表达式(或类型转换,如果指定)用冒号分隔。如果没有提供格式说明符,则使用空字符串。
因此,f-string 看起来像:
f ' <text> { <expression> <optional !s, !r, or !a> <optional : format specifier> } <text> ... '
然后使用 __format__
协议格式化表达式,并将格式说明符作为参数。生成的值用于构建 f-string 的值。
请注意,__format__()
不会直接在每个值上调用。实际代码使用 type(value).__format__(value, format_spec)
或 format(value, format_spec)
的等价形式。有关更多详细信息,请参阅内置 format()
函数的文档。
表达式不能在字符串、括号、方括号或大括号之外包含 ':'
或 '!'
。例外情况是 '!='
运算符作为特例被允许。
转义序列
反斜杠不能出现在 f-string 的表达式部分内部,因此你不能使用它们来转义 f-string 内部的引号,例如:
>>> f'{\'quoted string\'}'
File "<stdin>", line 1
SyntaxError: f-string expression part cannot include a backslash
你可以在表达式中使用不同类型的引号:
>>> f'{"quoted string"}'
'quoted string'
反斜杠转义可以出现在 f-string 的字符串部分内部。
请注意,要在结果字符串值中出现字面量大括号,正确的方法是重复大括号:
>>> f'{{ {4*10} }}'
'{ 40 }'
>>> f'{{{4*10}}}'
'{40}'
与 Python 中的所有原始字符串一样,原始 f-string 不会进行转义处理:
>>> fr'x={4*10}\n'
'x=40\\n'
由于 Python 的字符串标记化规则,f-string f'abc {a['x']} def'
是无效的。标记器将其解析为 3 个标记:f'abc {a['
、x
和 ']} def'
。就像常规字符串一样,这不能通过使用原始字符串来修复。有几种正确的方法可以编写这个 f-string:使用不同的引号字符:
f"abc {a['x']} def"
或者使用三引号:
f'''abc {a['x']} def'''
代码等价性
用于实现 f-string 的具体代码未指定。但是,可以保证任何转换为字符串的嵌入值都将使用该值的 __format__
方法。这与 str.format()
用于将值转换为字符串的机制相同。
例如,这段代码:
f'abc{expr1:spec1}{expr2!r:spec2}def{expr3}ghi'
可能会被求值为:
'abc' + format(expr1, spec1) + format(repr(expr2), spec2) + 'def' + format(expr3) + 'ghi'
表达式求值
从字符串中提取的表达式在 f-string 出现的上下文中求值。这意味着表达式可以完全访问局部变量和全局变量。任何有效的 Python 表达式都可以使用,包括函数和方法调用。
由于 f-string 在源代码中字符串出现的位置进行求值,因此 f-string 没有额外的表达能力。也没有额外的安全问题:你也可以直接编写相同的表达式,而不是将其放在 f-string 中。
>>> def foo():
... return 20
...
>>> f'result={foo()}'
'result=20'
等同于
>>> 'result=' + str(foo())
'result=20'
表达式的解析等同于 ast.parse('(' + expression + ')', '<fstring>', 'eval')
[7]。
请注意,由于表达式在求值之前被隐式括号包围,因此表达式可以包含换行符。例如:
>>> x = 0
>>> f'''{x
... +1}'''
'1'
>>> d = {0: 'zero'}
>>> f'''{d[0
... ]}'''
'zero'
格式说明符
格式说明符也可以包含求值表达式。这允许以下代码:
>>> width = 10
>>> precision = 4
>>> value = decimal.Decimal('12.34567')
>>> f'result: {value:{width}.{precision}}'
'result: 12.35'
一旦格式说明符中的表达式被求值(如有必要),格式说明符就不会被 f-string 求值器解释。就像 str.format()
中一样,它们只是传递给被格式化对象的 __format__()
方法。
字符串拼接
相邻的 f-string 和常规字符串会进行拼接。常规字符串在编译时拼接,f-string 在运行时拼接。例如,表达式:
>>> x = 10
>>> y = 'hi'
>>> 'a' 'b' f'{x}' '{c}' f'str<{y:^4}>' 'd' 'e'
产生的值为:
'ab10{c}str< hi >de'
虽然这种运行时拼接的具体方法未指定,但上述代码可能求值为:
'ab' + format(x) + '{c}' + 'str<' + format(y, '^4') + '>de'
每个 f-string 在与相邻 f-string 拼接之前都完全求值。这意味着这个:
>>> f'{x' f'}'
是一个语法错误,因为第一个 f-string 没有包含闭合大括号。
错误处理
处理 f-string 时可能发生编译时或运行时错误。编译时错误仅限于在扫描 f-string 时可以检测到的错误。这些错误都会引发 SyntaxError
。
不匹配的大括号
>>> f'x={x'
File "<stdin>", line 1
SyntaxError: f-string: expecting '}'
无效表达式
>>> f'x={!x}'
File "<stdin>", line 1
SyntaxError: f-string: empty expression not allowed
运行时错误发生在求值 f-string 内部的表达式时。请注意,f-string 可以被多次求值,有时成功,有时会引发错误:
>>> d = {0:10, 1:20}
>>> for i in range(3):
... print(f'{i}:{d[i]}')
...
0:10
1:20
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
KeyError: 2
或
>>> for x in (32, 100, 'fifty'):
... print(f'x = {x:+3}')
...
'x = +32'
'x = +100'
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ValueError: Sign not allowed in string format specifier
表达式中开头的和结尾的空白字符被忽略
为了便于阅读,表达式中的前导和尾随空白字符将被忽略。这是在求值之前将表达式用括号括起来的副产品。
表达式的求值顺序
f-string 中的表达式按从左到右的顺序求值。只有当表达式有副作用时才能检测到这一点:
>>> def fn(l, incr):
... result = l[0]
... l[0] += incr
... return result
...
>>> lst = [0]
>>> f'{fn(lst,2)} {fn(lst,3)}'
'0 2'
>>> f'{fn(lst,2)} {fn(lst,3)}'
'5 7'
>>> lst
[10]
讨论
python-ideas 讨论
python-ideas 上的大部分讨论 [8] 都集中在三个问题上:
- 如何表示 f-string,
- 如何指定 f-string 中表达式的位置,以及
- 是否允许完整的 Python 表达式。
如何表示 f-string
由于编译器必须参与评估插值字符串中包含的表达式,因此必须有一种方法向编译器指示哪些字符串应该被评估。本 PEP 选择在字符串字面量前面加上一个引导的 'f'
字符。这类似于 'b'
和 'r'
前缀在编译时改变字符串本身含义的方式。还建议了其他前缀,例如 'i'
。没有一个选项看起来比其他选项更好,因此选择了 'f'
。
另一个选择是支持编译器已知的特殊函数,例如 Format()
。这对于 Python 来说似乎过于魔法:不仅存在与现有标识符冲突的可能性,PEP 作者认为用字符串前缀字符来表示魔法更好。
如何在 f-string 中指定表达式的位置
本 PEP 支持与 str.format()
相同的语法来区分字符串内的替换文本:表达式包含在大括号内。还提出了其他选项,例如 string.Template
的 $identifier
或 ${expression}
。
虽然 $identifier
无疑更受 shell 脚本编写者和某些其他语言用户熟悉,但在 Python 中 str.format()
被大量使用。快速搜索 Python 的标准库显示 string.Template
仅有少量使用,而 str.format()
有数百次使用。
另一个提议的替代方案是将替换文本放在 \{
和 }
之间,或者 \{
和 \}
之间。虽然如果所有字符串字面量都支持插值,这种语法可能更可取,但本 PEP 仅支持已用前导 'f'
标记的字符串。因此,本 PEP 使用未修饰的大括号来表示替换文本,以利用最终用户对 str.format()
的熟悉度。
支持完整的 Python 表达式
许多在 python-ideas 讨论会上的人希望只支持单个标识符,或者支持 Python 表达式的有限子集(例如 str.format()
支持的子集)。本 PEP 支持大括号内的完整 Python 表达式。如果没有完整的表达式,一些期望的用法会很麻烦。例如:
>>> f'Column={col_idx+1}'
>>> f'number of items: {len(items)}'
将变成:
>>> col_number = col_idx+1
>>> f'Column={col_number}'
>>> n_items = len(items)
>>> f'number of items: {n_items}'
虽然 F-string 中确实可以包含非常难看的表达式,但本 PEP 认为此类用法应通过代码检查工具或代码审查来解决。
>>> f'mapping is { {a:b for (a, b) in ((1, 2), (3, 4))} }'
'mapping is {1: 2, 3: 4}'
其他语言中的类似支持
维基百科对其他编程语言中的字符串插值有很好的讨论 [9]。此功能在许多语言中实现,具有各种语法和限制。
f-string 和 str.format 表达式之间的差异
str.format()
中允许的有限表达式与 f-string 中允许的完整表达式之间有一个微小的差异。这种差异体现在索引查找的执行方式上。在 str.format()
中,看起来不像数字的索引值会转换为字符串:
>>> d = {'a': 10, 'b': 20}
>>> 'a={d[a]}'.format(d=d)
'a=10'
注意,当在字典中查找时,索引值被转换为字符串 'a'
。
然而,在 f-string 中,你需要为 'a'
的值使用字面量:
>>> f'a={d["a"]}'
'a=10'
这种差异是必需的,否则你将无法使用变量作为索引值:
>>> a = 'b'
>>> f'a={d[a]}'
'a=20'
有关进一步讨论,请参阅 [10]。正是这一观察导致了 f-string 中支持完整的 Python 表达式。
此外,str.format()
所理解的有限表达式不一定是有效的 Python 表达式。例如:
>>> '{i[";]}'.format(i={'";':4})
'4'
因此,str.format() 的“表达式解析器”不适用于实现 f-string。
三引号 f-string
允许使用三引号 f-string。这些字符串的解析方式与普通三引号字符串相同。解析和解码后,应用正常的 f-string 逻辑,并对每个值调用 __format__()
。
原始 f-string
原始字符串和 f-string 可以结合使用。例如,它们可以用于构建正则表达式:
>>> header = 'Subject'
>>> fr'{header}:\s+'
'Subject:\\s+'
此外,原始 f-string 可以与三引号字符串结合使用。
不支持二进制 f-string
由于我们不支持 bytes.format()
的原因,你不能将 'f'
与 'b'
字符串字面量结合使用。主要问题是对象的 __format__()
方法可能返回与字节字符串不兼容的 Unicode 数据。
二进制 f-string 首先需要解决 bytes.format()
的问题。过去曾提出过这个想法,最近一次是在 PEP 461 中。对此类功能的讨论通常建议以下两种方式之一:
- 添加一个像
__bformat__()
这样的方法,以便对象可以控制它如何转换为字节,或者 - 让
bytes.format()
不像str.format()
那样通用或可扩展。
如果将来需要此类功能,这两种方式仍然是可选项。
!s
、!r
和 !a
是多余的
!s
、!r
和 !a
转换并非严格必需。因为在 f-string 内部允许任意表达式,所以这段代码:
>>> a = 'some string'
>>> f'{a!r}'
"'some string'"
与以下代码相同:
>>> f'{repr(a)}'
"'some string'"
类似地,!s
可以替换为对 str()
的调用,!a
可以替换为对 ascii()
的调用。
然而,本 PEP 支持 !s
、!r
和 !a
,以尽量减少与 str.format()
的差异。str.format()
中需要 !s
、!r
和 !a
,因为它不允许执行任意表达式。
表达式中的 Lambda 表达式
由于 lambda 表达式使用了 ':'
字符,它们不能在表达式的括号外部出现。冒号被解释为格式说明符的开始,这意味着 lambda 表达式的开始被视为语法无效。由于在 f-string 表达式中没有实际使用纯 lambda 表达式的场景,这并没有被视为很大的限制。
如果你觉得必须使用 lambda 表达式,它们可以在括号内使用:
>>> f'{(lambda x: x*2)(3)}'
'6'
不能与 'u' 结合
“u”前缀是在 PEP 414 中添加到 Python 3.3 的,作为缓解与 Python 2.7 源代码兼容性的一种手段。由于 Python 2.7 永远不会支持 f-string,因此将“f”前缀与“u”结合起来并没有任何好处。
Python 源代码中的示例
以下是 Python 源代码中目前使用 str.format()
的一些示例,以及它们使用 f-string 后的样子。本 PEP 不建议全面转换为 f-string,这些只是 str.format()
实际用法的示例,以及如果从头开始使用 f-string 编写它们会是什么样子。
Lib/asyncio/locks.py
:
extra = '{},waiters:{}'.format(extra, len(self._waiters))
extra = f'{extra},waiters:{len(self._waiters)}'
Lib/configparser.py
:
message.append(" [line {0:2d}]".format(lineno))
message.append(f" [line {lineno:2d}]")
Tools/clinic/clinic.py
:
methoddef_name = "{}_METHODDEF".format(c_basename.upper())
methoddef_name = f"{c_basename.upper()}_METHODDEF"
python-config.py
:
print("Usage: {0} [{1}]".format(sys.argv[0], '|'.join('--'+opt for opt in valid_opts)), file=sys.stderr)
print(f"Usage: {sys.argv[0]} [{'|'.join('--'+opt for opt in valid_opts)}]", file=sys.stderr)
参考资料
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0498.rst