Python PEP 822 提案:新增自动缩进移除的多行字符串语法

3 阅读7分钟

PEP 822 – 消除缩进的多行字符串(d-string)

作者: Inada Naoki 讨论渠道: Discourse 主题 状态: 草案 类型: 标准追踪 创建时间: 2026-01-05 Python版本: 3.15 提交历史: 2026-01-05

摘要

本 PEP 提议增加一个功能,用于自动从多行字符串字面量中移除缩进。 消除缩进的多行字符串(dedented multiline strings)使用一个新的前缀“d”(“dedent”的简写),放在多行字符串字面量的开引号之前。 示例(空格用下划线 _ 可视化):

def hello_paragraph() -> str:
____return d"""
________<p>
__________Hello, World!
________</p>
____"""

末尾的三引号决定了要移除多少缩进。 在上面的例子中,返回的字符串将包含三行:

"____<p>\n"(四个前导空格) "______Hello, World!\n"(六个前导空格) "____</p>\n"(四个前导空格)

动机

在深度缩进的 Python 代码中编写多行字符串字面量时,用户面临以下选择:

  1. 接受字符串字面量的内容将左对齐。
  2. 使用多个单行字符串字面量拼接,而不是使用一个多行字符串字面量。
  3. 使用 textwrap.dedent() 来移除缩进。

所有这些选项在代码可读性和可维护性方面都有缺点。

  • 左对齐的多行字符串看起来不协调,往往被避免使用。在实践中,包括 Python 自己的测试代码在内的许多地方都选择其他方法。
  • 拼接的单行字符串字面量更冗长,更难维护。
  • textwrap.dedent() 是用 Python 实现的,因此需要一些运行时开销。它不能在性能关键的热路径中使用。

本 PEP 旨在为消除缩进的多行字符串提供一种内置语法,它既易于读写,在运行时也高效。

原理

此想法的主要替代方案是用 C 语言实现 textwrap.dedent() 并将其作为 str.dedent() 方法提供。 这种方法可以减少 textwrap.dedent() 的运行时开销。通过使其成为内置方法,当直接在字符串字面量上调用时,还允许进行编译时缩进移除。 然而,这种方法有几个缺点:

  • 为了支持用户希望在字符串中包含某些缩进的情况,dedent() 方法需要接受一个参数来指定要移除的缩进量。这对用户来说将是繁琐且容易出错的。
  • 当使用续行符(行尾带有反斜杠的行)时,它们无法被消除缩进。
  • f-字符串可能会将表达式插值为多行字符串而没有缩进。在这种情况下,f-string + str.dedent() 无法消除整个字符串的缩进。
  • t-字符串不创建 str 对象,因此它们不能使用 str.dedent() 方法。虽然可以向 string.templatelib.Template 添加 dedent() 方法,但这会导致不一致,因为 t-字符串和 f-字符串非常相似,但在消除缩进方面会有不同的行为。

str.dedent() 方法对于非字面量字符串仍然有用,因此本 PEP 并不排斥这个想法。 然而,为了便于在多行字符串字面量中使用,提供专用的语法更为优越。

规范

为消除缩进的多行字符串添加一个新的字符串字面量前缀“d”。 该前缀可以与“f”、“t”和“r”前缀组合使用。 该前缀仅用于多行字符串字面量。 因此,它只能与三引号("""''')一起使用。 在单引号或双引号("')上使用它是一个语法错误。 开头的三引号后必须跟一个换行符。此换行符不包含在结果字符串中。 要移除的缩进量由紧接在末尾三引号之前的空白字符(空格 ' ' 或制表符 '\t')决定。 在缩进中混合使用空格和制表符会引发 TabError,类似于 Python 自己的缩进规则。 消除缩进的过程从字符串中的每一行移除确定的先导空白字符量。 比确定的缩进量短的行会变成一个空行(例如 "\n")。 否则,如果该行不是以确定的缩进开始,Python 会引发 IndentationError。 除非与“r”前缀结合使用,反斜杠转义在处理完移除缩进后才被处理。 因此,你不能使用 \t 来创建缩进。 并且你可以使用行延续符(行尾的反斜杠)并从延续行中移除缩进。

示例: (为清晰起见,空格显示为 _,制表符显示为 --->。错误消息仅为解释,实际消息可能不同。)

s = d""  # SyntaxError: d-string must be a multiline string
s = d"""Hello"""  # SyntaxError: d-string must be a multiline string
s = d"""Hello
__World!
"""  # SyntaxError: d-string must start with a newline

s = d"""
__Hello
__World!"""  # SyntaxError: d-string must end with an indent-only line

s = d"""
__Hello
__World!
"""  # 不移除缩进,因为末尾引号没有缩进。
print(repr(s))  # '__Hello\n__World!\n'

s = d"""
__Hello
__World!
_"""  # 移除一个空格的缩进。
print(repr(s))  # '_Hello\n_World!\n'

s = d"""
__Hello
__World!
__"""  # 移除两个空格的缩进。
print(repr(s))  # 'Hello\nWorld!\n'

s = d"""
__Hello
__World!
___"""  # IndentationError: missing valid indentation

s = d"""
>--->__Hello
>__World!
>"""  # 允许制表符作为缩进。
             # 空格只是字符串的一部分,不是要移除的缩进。
print(repr(s))  # '__Hello\n__World!\n'

s = d"""
>____World!
--->__"""  # TabError: mixing spaces and tabs in indentation

s = d"""
__Hello \
__World!\
__"""  # 行延续符正常工作
print(repr(s))  # 'Hello_World!'

s = d"""\
__Hello
__World
__"""  # SyntaxError: d-string must starts with a newline.

s = dr"""
__Hello\
__World!\
__"""  # d-string 可以与 r-string 结合。
print(repr(s))  # 'Hello\\\nWorld!\\\n'

s = df"""
____Hello, {"world".title()}!
____"""  # d-string 也可以与 f-string 和 t-string 结合。
print(repr(s))  # 'Hello, World!\n'

s = dt"""
____Hello, {"world".title()}!
____"""
print(type(s))  # <class 'string.templatelib.Template'>
print(s.strings)  # ('Hello, ', '!\n')
print(s.values)  # ('World',)
print(s.interpolations)
# (Interpolation('World', '"world".title()', None, ''),)

如何教授此功能

在教程中,可以结合三引号字符串字面量来介绍 d-string。 此外,可以在 textwrap.dedent() 的文档中添加一个注释,提供指向语言参考中 d-string 部分或教程相关部分的链接。

其他具有类似功能的语言

  • Java 15 引入了一个称为文本块(text blocks)的功能。由于 Java 以前没有使用三引号,他们引入了用于多行字符串字面量的三引号,并自动移除缩进。
  • C# 11 也引入了一个类似的功能,称为原始字符串字面量(raw string literals)。
  • JuliaSwift 也支持自动移除缩进的三引号字符串字面量。
  • PHP 7.3 引入了灵活的 Heredoc 和 Nowdoc 语法。虽然它使用结束标记(例如 <<<END ... END)而不是三引号,但它也会从文本中移除缩进。

Java 和 Julia 使用缩进最少的那一行来确定要移除的缩进量。 Swift、C# 和 PHP 使用末尾三引号或结束标记的缩进。 本 PEP 选择了 Swift 和 C# 的方法,因为它更简单且更容易解释。

参考实现

PEP 822 的一个 CPython 实现可在 methane/cpython#108 获取。

被拒绝的想法

str.dedent() 方法

正如原理部分提到的,本 PEP 并不拒绝 str.dedent() 方法这个想法。 一个用 C 实现的更快的 textwrap.dedent() 版本对运行时缩进移除会很有用。 然而,d-string 更适合多行字符串字面量,因为:

  • 它能很好地与 f/t-strings 配合工作。
  • 它允许更轻松地指定要移除的缩进量。
  • 它可以消除续行符的缩进。

三重反引号(Triple-backtick)

考虑使用三重反引号作为消除缩进的多行字符串的替代语法。 这种符号在 Markdown 中为我们所熟悉。虽然过去存在对某些键盘布局的担忧,但如今许多人已经习惯输入这种符号。 然而,当在 Markdown 中嵌入 Python 代码或反之亦然时,这种符号会产生冲突。 因此,考虑到这些缺点,增加引号字符的种类被认为不如为字符串字面量添加前缀优越。

__future__ 导入

除了为字符串字面量添加前缀之外,还考虑过使用 __future__ 导入来改变多行字符串字面量的默认行为。 这可能有助于将来简化 Python 的语法。 但是,将所有现有的复杂代码库重写为新符号可能并不简单。 在该源代码中的所有多行字符串都被重写为新符号之前,无法利用自动缩进移除功能。 在所有用户能够将现有代码库重写为新符号之前,两种类型的 Python 语法将无限期共存。 因此,许多人更喜欢新的字符串前缀,而不是 __future__ 导入。

版权

本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。

来源: github.com/python/peps… 最后修改时间: 2026-01-07 15:13:47 GMT