PEP 750 – 模板字符串(Template Strings)

Python 增强提案

PEP 750 – 模板字符串(Template Strings)

作者:

Jim Baker <jim.baker at python.org>, Guido van Rossum , Paul Everitt , Koudai Aono , Lysandros Nikolaou , Dave Peck

讨论地址:

Discourse thread

状态:

已接受(Accepted)

类型:

标准跟踪(Standards Track)

创建时间:

2024-07-08

Python 版本:

3.14

发布历史:

2024-08-09, 2024-10-17, 2024-10-21, 2024-11-18

决议:

2025-04-10

目录

摘要

本 PEP 引入了用于自定义字符串处理的模板字符串。

模板字符串是 f-strings 的一种泛化,使用 t 代替 f 前缀。t-strings 不会求值为 str,而是求值为一种新类型 Template

template: Template = t"Hello {name}"

Template 为开发者提供了在组合之前访问字符串及其插值的能力。 这为 Python 语言带来了原生的灵活字符串处理,并支持安全检查、Web 模板、领域特定语言等。

与其他 PEP 的关系

Python 在 Python 3.6 中通过 PEP 498 引入了 f-strings。 然后,该语法在 PEP 701 中进行了形式化,该规范也取消了一些限制。 此 PEP 基于 PEP 701。

几乎在 PEP 498 到达的同时,编写了 PEP 501 以提供 “i-strings” – 即 “interpolation template strings”。 由于 f-strings 的进一步经验,该 PEP 被推迟。 2023 年 3 月,另一位作者恢复了此 PEP 的工作,引入了 “t-strings” 作为模板字面量字符串,并建立在 PEP 701 之上。

此 PEP 的作者认为它是 PEP 501 中更新工作的概括和简化。(该 PEP 最近也进行了更新,以反映此 PEP 中的新想法。)

动机

Python f-strings 易于使用且非常受欢迎。 然而,随着时间的推移,开发人员遇到了使其不适合某些用例的限制。 特别是,f-strings 无法在插值合并到最终字符串之前拦截和转换它们。

因此,不小心使用 f-strings 可能会导致安全漏洞。 例如,使用 [sqlite3](https://peps.python.org/pep-0750/https:/docs.python.org/3/library/sqlite3.html#module-sqlite3 "(in Python v3.13)") 的用户在执行 SQL 查询时,可能会尝试使用 f-string 将值嵌入到他们的 SQL 表达式中,这可能会导致 SQL 注入攻击。 或者,构建 HTML 的开发人员可能会在字符串中包含未转义的用户输入,从而导致 跨站脚本 (XSS) 漏洞。

更广泛地说,在插值合并到最终字符串之前转换它们的能力的缺失限制了 f-strings 在更复杂的字符串处理任务中的效用。

模板字符串通过为开发者提供对字符串及其插值的访问来解决这些问题。

例如,假设我们要生成一些 HTML。 使用模板字符串,我们可以定义一个 html() 函数,该函数允许我们自动清理内容:

evil = "<script>alert('evil')</script>"
template = t"<p>{evil}</p>"
assert html(template) == "<p>&lt;script&gt;alert('evil')&lt;/script&gt;</p>"

同样,我们假设的 html() 函数可以使开发人员使用字典轻松地将属性添加到 HTML 元素:

attributes = {"src": "shrubbery.jpg", "alt": "looks nice"}
template = t"<img {attributes} />"
assert html(template) == '<img src="shrubbery.jpg" alt="looks nice" />'

这些示例都无法使用 f-strings 实现。 通过提供拦截和转换插值的机制,模板字符串支持广泛的字符串处理用例。

规范

模板字符串字面量

本 PEP 引入了一个新的字符串前缀 t,用于定义模板字符串字面量。 这些字面量解析为标准库模块 string.templatelib 中的一种新类型 Template

以下代码创建一个 Template 实例:

from string.templatelib import Template
template = t"This is a template string."
assert isinstance(template, Template)

模板字符串字面量支持 PEP 701 的完整语法。 这包括在插值中嵌套模板字符串的能力,以及使用所有有效的引号('"'''""")的能力。 与其他字符串前缀一样,t 前缀必须紧接在引号之前。 与 f-strings 一样,支持小写 t 和大写 T 前缀。 与 f-strings 一样,t-strings 不能与 ub 前缀组合使用。

此外,f-strings 和 t-strings 不能组合,因此 ft 前缀无效。 t-strings 可以r 前缀组合使用; 有关更多信息,请参见下面的原始模板字符串部分。

Template 类型

模板字符串会求值为一个新的不可变类型 string.templatelib.Template 的实例:

class Template:
  strings: tuple[str, ...]
"""
  模板的字符串部分的非空元组,具有 N+1 个项目,其中 N 是模板中插值的数量。
"""
  interpolations: tuple[Interpolation, ...]
"""
  模板的插值部分的元组。 如果没有插值,这将是一个空元组。
"""
  def __new__(cls, *args: str | Interpolation):
"""
    创建一个新的 Template 实例。
    可以按任何顺序提供参数。
"""
    ...
  @property
  def values(self) -> tuple[object, ...]:
"""
    返回模板中每个 Interpolation 的 `value` 属性的元组。
    如果没有插值,这将是一个空元组。
"""
    ...
  def __iter__(self) -> Iterator[str | Interpolation]:
"""
    迭代模板中的字符串部分和插值。
    这些可以按任何顺序出现。 不会包含空字符串。
"""
    ...

stringsinterpolations 属性提供对字符串部分和字面量中任何插值的访问:

name = "World"
template = t"Hello {name}"
assert template.strings[0] == "Hello "
assert template.interpolations[0].value == "World"

Interpolation 类型

Interpolation 类型表示模板字符串中的表达式。 与 Template 一样,它是在 string.templatelib 模块中找到的一个新类:

class Interpolation:
  value: object
  expression: str
  conversion: Literal["a", "r", "s"] | None
  format_spec: str
  __match_args__ = ("value", "expression", "conversion", "format_spec")
  def __new__(
    cls,
    value: object,
    expression: str,
    conversion: Literal["a", "r", "s"] | None = None,
    format_spec: str = "",
  ):
    ...

Interpolation 类型是浅不可变的。 无法重新分配其属性。

value 属性是插值的求值结果:

name = "World"
template = t"Hello {name}"
assert template.interpolations[0].value == "World"

expression 属性是插值的 原始文本

name = "World"
template = t"Hello {name}"
assert template.interpolations[0].expression == "name"

我们希望 expression 属性不会在大多数模板处理代码中使用。 提供它是为了完整起见,以及用于调试和内省。 有关如何处理模板字符串的更多信息,请参见处理模板时常见的模式部分和示例部分。

conversion 属性是要使用的可选转换,它是 rsa 之一,分别对应于 repr()str()ascii() 转换。 与 f-strings 一样,不支持其他转换:

name = "World"
template = t"Hello {name!r}"
assert template.interpolations[0].conversion == "r"

如果没有提供转换,则 conversionNone

format_spec 属性是格式规范。 与 f-strings 一样,这是一个任意字符串,用于定义如何呈现该值:

value = 42
template = t"Value: {value:.2f}"
assert template.interpolations[0].format_spec == ".2f"

f-strings 中的格式规范本身可以包含插值。 这在模板字符串中也是允许的; format_spec 设置为急切求值的结果:

value = 42
precision = 2
template = t"Value: {value:.{precision}f}"
assert template.interpolations[0].format_spec == ".2f"

如果没有提供格式规范,则 format_spec 默认为空字符串("")。 这与 Python 的 [format()](https://peps.python.org/pep-0750/https:/docs.python.org/3/library/functions.html#format "(in Python v3.13)") 内置函数的 format_spec 参数匹配。

与 f-strings 不同,由处理模板的代码来确定如何解释 conversionformat_spec 属性。 此类代码不是必需使用这些属性,但存在时应尊重它们,并在可能的范围内匹配 f-strings 的行为。 例如,如果一个使用 {value:.2f} 的模板字符串在处理时没有将该值四舍五入到小数点后两位,那将令人惊讶。

Template.values 属性

Template.values 属性是访问模板中每个 Interpolationvalue 属性的快捷方式,等效于:

@property
def values(self) -> tuple[object, ...]:
  return tuple(i.value for i in self.interpolations)

迭代 Template 内容

Template.__iter__() 方法提供了一种访问模板完整内容的简单方法。 它按出现的顺序生成字符串部分和插值,省略空字符串。

__iter__() 方法等效于:

def __iter__(self) -> Iterator[str | Interpolation]:
  for s, i in zip_longest(self.strings, self.interpolations):
    if s:
      yield s
    if i:
      yield i

以下示例显示了 __iter__() 方法的实际应用:

assert list(t"") == []
assert list(t"Hello") == ["Hello"]
name = "World"
template = t"Hello {name}!"
contents = list(template)
assert len(contents) == 3
assert contents[0] == "Hello "
assert contents[1].value == "World"
assert contents[1].expression == "name"
assert contents[2] == "!"

Template.strings 中可能存在的空字符串不包含在 __iter__() 方法的输出中:

first = "Eat"
second = "Red Leicester"
template = t"{first}{second}"
contents = list(template)
assert len(contents) == 2
assert contents[0].value == "Eat"
assert contents[0].expression == "first"
assert contents[1].value == "Red Leicester"
assert contents[1].expression == "second"
# 但是,strings 属性包含空字符串:
assert template.strings == ("", "", "")

模板处理代码可以选择根据要求和便利性使用 stringsinterpolationsvalues__iter__() 的任意组合。

处理模板字符串

开发人员可以编写任意代码来处理模板字符串。 例如,以下函数将模板的静态部分呈现为小写,并将插值呈现为大写:

from string.templatelib import Template, Interpolation
def lower_upper(template: Template) -> str:
"""将静态部分渲染为小写,插值渲染为大写。"""
  parts: list[str] = []
  for item in template:
    if isinstance(item, Interpolation):
      parts.append(str(item.value).upper())
    else:
      parts.append(item.lower())
  return "".join(parts)
name = "world"
assert lower_upper(t"HELLO {name}") == "hello WORLD"

不要求以任何特定方式处理模板字符串。 处理模板的代码没有义务返回字符串。 模板字符串是一种灵活的通用功能。

有关如何处理模板字符串的更多信息,请参见处理模板时常见的模式部分。 有关详细的工作示例,请参见示例部分。

模板字符串拼接

模板字符串支持使用 + 进行显式拼接。 支持两个 Template 实例的拼接,以及 Template 实例和 str 的拼接:

name = "World"
template = t"{name}"
assert isinstance(t"Hello " + template, Template)
assert (t"Hello " + template).strings == ("Hello ", "")
assert (t"Hello " + template).interpolations[0].value == "World"
assert isinstance("Hello " + template, Template)
assert ("Hello " + template).strings == ("Hello ", "")
assert ("Hello " + template).interpolations[0].value == "World"

模板的拼接是“病毒式”的:Templatestr 的拼接始终会生成 Template 实例。

Python 的隐式拼接语法也受支持。 以下代码将按预期工作:

name = "World"
assert (t"Hello " t"World").strings == ("Hello World",)
assert ("Hello " t"World").strings == ("Hello World",)

Template 类型支持两个 Template 实例之间以及 Template 实例和 str 之间的 __add__()__radd__() 方法。

TemplateInterpolation 的相等性

TemplateInterpolation 实例使用对象标识 (is) 进行比较。

Template 实例旨在由模板处理代码使用,该代码可以返回字符串或任何其他类型。 这些类型可以根据需要提供自己的相等性语义。

不支持排序

TemplateInterpolation 类型不支持排序。 这与 Python 中的所有其他字符串字面量类型不同,后者支持字典排序。 由于插值可以包含任意值,因此没有它们的自然排序。 因此,TemplateInterpolation 类型均未实现标准比较方法。

支持调试说明符 (=)

调试说明符 = 在模板字符串中受支持,并且其行为与在 f-strings 中的行为类似,但由于实现的限制,存在细微的差异。

特别是,t'{value=}' 被视为 t'value={value!r}'

name = "World"
template = t"Hello {name=}"
assert template.strings[0] == "Hello name="
assert template.interpolations[0].value == "World"
assert template.interpolations[0].conversion == "r"

如果还提供了单独的格式字符串,则 t'{value=:fmt} 将被视为 t'value={value!s:fmt}'

空格保留在调试说明符中,因此 t'{value = }' 被视为 t'value = {value!r}'

原始模板字符串

使用 rt(或 tr)前缀支持原始模板字符串:

trade = 'shrubberies'
template = rt'Did you say "{trade}"?\n'
assert template.strings[0] == r'Did you say "'
assert template.strings[1] == r'"?\n'

在此示例中,\n 被视为两个单独的字符(反斜杠后跟“n”),而不是换行符。 这与 Python 的原始字符串行为一致。

与常规模板字符串一样,原始模板字符串中的插值会正常处理,从而允许原始字符串行为和动态内容的组合。

插值表达式求值

插值的表达式求值与 PEP 498 中的相同:

从字符串中提取的表达式在其出现的模板字符串的上下文中进行求值。 这意味着表达式可以完全访问其词法范围,包括局部变量和全局变量。 可以使用任何有效的 Python 表达式,包括函数和方法调用。

模板字符串像 f-strings 一样从左到右进行急切求值。 这意味着插值在处理模板字符串时会立即求值,而不是延迟或包装在 lambdas 中。

异常

t-string 字面量中引发的异常与 f-string 字面量中引发的异常相同。

没有 Template.__str__() 实现

Template 类型不提供专门的 __str__() 实现。

这是因为 Template 实例旨在由模板处理代码使用,该代码可以返回字符串或任何其他类型。 没有将 Template 转换为字符串的规范方法。

TemplateInterpolation 类型都提供了有用的 __repr__() 实现。

string.templatelib 模块

[string](https://peps.python.org/pep-0750/https:/docs.python.org/3/library/string.html#module-string "(in Python v3.13)") 模块将转换为一个包,其中一个新的 templatelib 子模块包含 TemplateInterpolation 类型。 在实现此 PEP 之后,此新模块可用于相关函数,例如 convert(),或潜在的未来模板处理代码,例如 shell 脚本助手。

示例

本 PEP 的本节中的所有示例都已在公共 pep750-examples git 存储库中提供完全测试的参考实现。

示例:使用 t-strings 实现 f-strings

使用 t-strings “实现” f-strings 很容易。 也就是说,我们可以编写一个函数 f(template: Template) -> str,该函数以与 f-string 字面量非常相似的方式处理 Template,并返回相同的结果:

name = "World"
value = 42
templated = t"Hello {name!r}, value: {value:.2f}"
formatted = f"Hello {name!r}, value: {value:.2f}"
assert f(templated) == formatted

f() 函数支持转换说明符(如 !r)和格式说明符(如 :.2f)。 完整的代码非常简单:

from string.templatelib import Template, Interpolation
def convert(value: object, conversion: Literal["a", "r", "s"] | None) -> object:
  if conversion == "a":
    return ascii(value)
  elif conversion == "r":
    return repr(value)
  elif conversion == "s":
    return str(value)
  return value
def f(template: Template) -> str:
  parts = []
  for item in template:
    match item:
      case str() as s:
        parts.append(s)
      case Interpolation(value, _, conversion, format_spec):
        value = convert(value, conversion)
        value = format(value, format_spec)
        parts.append(value)
  return "".join(parts)

注意

示例代码

参见 fstring.pytest_fstring.py

[示例:结构化日志](https://peps.python.org