Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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-字符串”,取自用于表示此类字符串的前导字符,代表“格式化字符串”。

本 PEP 并不建议删除或弃用任何现有的字符串格式化机制。

F-字符串提供了一种在字符串字面量内嵌入表达式的方法,使用最少的语法。需要注意的是,f-字符串实际上是在运行时计算的表达式,而不是常量值。在 Python 源代码中,f-字符串是一个以 '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 表达式的一个子集,并且不支持类型特定的字符串格式化(__format__() 方法),该方法是在 PEP 3101 中引入的。

基本原理

本 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-字符串,这将变成

>>> f'The value is {value}.'
'The value is 80.'

F-字符串提供了一种简洁易读的方式,可以在字符串中包含 Python 表达式的值。

从这个意义上说,string.Template 和 %-格式化与 str.format() 具有类似的缺点,但也支持更少的格式化选项。特别是,它们不支持 __format__ 协议,因此无法控制如何将特定对象转换为字符串,也不能将其扩展到希望控制如何将其转换为字符串的其他类型(例如 Decimaldatetime)。此示例在 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-字符串是以字母 'f' 或 'F' 为前缀的字符串字面量。本 PEP 在任何地方使用 'f' 时,也可以使用 'F'。'f' 可以与 'r' 或 'R' 组合,以任意顺序组合,以生成原始 f-字符串字面量。'f' 不能与 'b' 组合:本 PEP 并不建议添加二进制 f-字符串。'f' 不能与 'u' 组合。

在标记源文件时,f-字符串使用与普通字符串、原始字符串、二进制字符串和三引号字符串相同的规则。也就是说,字符串必须以与开头相同的字符结尾:如果它以单引号开头,则必须以单引号结尾,依此类推。这意味着任何当前扫描 Python 代码以查找字符串的代码都应该很容易修改以识别 f-字符串(当然,f-字符串内的解析是另一回事)。

一旦标记化,f-字符串就会被解析成字面量字符串和表达式。表达式出现在花括号 '{''}' 内。在扫描字符串以查找表达式时,f-字符串的字面量部分内的任何双花括号 '{{''}}' 将被替换为相应的单个花括号。双字面量起始花括号并不表示表达式的开始。字符串的字面量部分中的单个结束花括号 '}' 是一个错误:为了表示单个结束花括号,字面量结束花括号必须是双重的 '}}'

f-字符串中花括号之外的部分是字面量字符串。然后对这些字面量部分进行解码。对于非原始 f-字符串,这包括将反斜杠转义序列(如 '\n''\"'"\'"'\xhh''\uxxxx''\Uxxxxxxxx' 和命名 Unicode 字符 '\N{name}')转换为其关联的 Unicode 字符 [6]

反斜杠不能出现在表达式中的任何位置。表达式内部不允许使用 '#' 字符的注释。

在每个表达式之后,可以指定可选的类型转换。允许的转换是 '!s''!r''!a'。这些与 str.format() 中的处理方式相同:'!s' 对表达式调用 str()'!r' 对表达式调用 repr()'!a' 对表达式调用 ascii()。这些转换是在调用 format() 之前应用的。使用 '!s' 的唯一原因是,如果您想指定一个应用于 str 的格式说明符,而不是应用于表达式的类型。

F-字符串使用与 str.format 相同的格式说明符迷你语言。与 str.format() 类似,可选的格式说明符可以包含在 f-字符串中,并使用冒号与表达式(或指定的类型转换)分隔。如果没有提供格式说明符,则使用空字符串。

因此,f-字符串如下所示:

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

然后使用 __format__ 协议使用格式说明符作为参数对表达式进行格式化。生成的 value 用于构建 f-字符串的值。

请注意,不会直接对每个值调用 __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-字符串

因为编译器必须参与评估内插字符串中包含的表达式,所以必须有一些方法来指示编译器哪些字符串应该被评估。此 PEP 选择了在字符串文字前面添加 'f' 字符。这类似于 'b''r' 前缀如何在编译时更改字符串本身的含义。还建议使用其他前缀,例如 'i'。没有一个选项比其他选项更好,因此选择了 'f'

另一个选择是支持编译器已知的特殊函数,例如 Format()。这对于 Python 来说似乎太神奇了:不仅有可能与现有的标识符发生冲突,PEP 作者认为最好用字符串前缀字符来表示这种神奇之处。

如何在 f-字符串中指定表达式的 位置

此 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 认为应该在 linter 或代码审查中解决此类用法。

>>> f'mapping is { {a:b for (a, b) in ((1, 2), (3, 4))} }'
'mapping is {1: 2, 3: 4}'

其他语言中的类似支持

维基百科对其他编程语言中的字符串插值进行了很好的讨论 [9]。此功能在许多语言中实现,并具有各种语法和限制。

f-字符串和 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-字符串

允许使用三引号 f-string。这些字符串的解析方式与普通的三引号字符串相同。解析和解码后,将应用正常的 f-string 逻辑,并对每个值调用 __format__()

原始 f-字符串

可以组合使用原始 f-string 和 f-string。例如,它们可以用于构建正则表达式。

>>> header = 'Subject'
>>> fr'{header}:\s+'
'Subject:\\s+'

此外,原始 f-string 可以与三引号字符串组合。

没有二进制 f-字符串

由于我们不支持 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() 的调用。

然而,为了最大程度地减少与str.format()的差异,本 PEP 支持!s!r!a。在str.format()中需要!s!r!a,因为它不允许执行任意表达式。

表达式内的 lambda 函数

由于 lambda 使用':'字符,因此它们不能出现在表达式中的括号之外。冒号被解释为格式说明符的开始,这意味着 lambda 表达式的开始被识别并且语法无效。由于在 f-string 表达式中没有对普通 lambda 的实际用途,因此这并没有被视为很大的限制。

如果您必须使用 lambda,则可以在括号内使用它们。

>>> f'{(lambda x: x*2)(3)}'
'6'

不能与 'u' 组合使用

在 Python 3.3 中,PEP 414添加了“u”前缀,作为一种简化与 Python 2.7 源代码兼容性的方法。由于 Python 2.7 永远不会支持 f-string,因此能够将“f”前缀与“u”结合起来没有任何好处。

Python 源代码中的示例

以下是一些当前使用str.format()的 Python 源代码示例,以及它们使用 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

上次修改时间:2023-09-09 17:39:29 GMT