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-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__ 协议,因此无法控制特定对象如何转换为字符串,也不能扩展到其他需要控制如何转换为字符串的类型(例如 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-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

最后修改:2025-02-01 08:59:27 GMT