PEP 750 – 模板字符串
- 作者:
- Jim Baker <jim.baker at python.org>,Guido van Rossum <guido at python.org>,Paul Everitt <pauleveritt at me.com>,Koudai Aono <koxudaxi at gmail.com>,Lysandros Nikolaou <lisandrosnik at gmail.com>,Dave Peck <davepeck at davepeck.org>
- 讨论至:
- Discourse 帖子
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2024年7月8日
- Python 版本:
- 3.14
- 发布历史:
- 2024年8月9日,2024年10月17日,2024年10月21日,2024年11月18日
- 决议:
- 2025年4月10日
摘要
本 PEP 引入了模板字符串用于自定义字符串处理。
模板字符串是 f-字符串的泛化,使用 t 代替 f 前缀。t-字符串不是求值为 str,而是求值为一个新类型 Template。
template: Template = t"Hello {name}"
模板让开发者在字符串及其插值组合之前就能访问它们。这为 Python 语言带来了原生的灵活字符串处理,并实现了安全检查、Web 模板、领域特定语言等功能。
与其他 PEP 的关系
Python 在 Python 3.6 中通过 PEP 498 引入了 f-字符串。其语法随后在 PEP 701 中被形式化,该 PEP 也取消了一些限制。本 PEP 基于 PEP 701。
在 PEP 498 发布的同时,PEP 501 被编写,旨在提供“i-字符串”——即“插值模板字符串”。该 PEP 被推迟,等待 f-字符串的进一步经验。本 PEP 的工作在 2023 年 3 月由另一位作者恢复,引入了“t-字符串”作为模板字面量字符串,并建立在 PEP 701 之上。
本 PEP 的作者认为它是 PEP 501 中更新工作的泛化和简化。(该 PEP 最近也进行了更新,以反映本 PEP 中的新思想。)
动机
Python f-字符串易于使用且非常流行。然而,随着时间的推移,开发者遇到了使其 不适用于某些用例 的限制。特别是,f-字符串无法在插值组合成最终字符串之前拦截并转换它们。
因此,不小心使用 f-字符串可能导致安全漏洞。例如,用户使用 sqlite3 执行 SQL 查询时,可能会倾向于使用 f-字符串将值嵌入到 SQL 表达式中,这可能导致 SQL 注入攻击。或者,开发者构建 HTML 时,可能在字符串中包含未转义的用户输入,导致 跨站脚本 (XSS) 漏洞。
更广泛地说,在插值组合成最终字符串之前无法对其进行转换,这限制了 f-字符串在更复杂的字符串处理任务中的实用性。
模板字符串通过让开发者访问字符串及其插值来解决这些问题。
例如,假设我们要生成一些 HTML。使用模板字符串,我们可以定义一个 html() 函数,它允许我们自动清理内容。
evil = "<script>alert('evil')</script>"
template = t"<p>{evil}</p>"
assert html(template) == "<p><script>alert('evil')</script></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-字符串实现。通过提供一种拦截和转换插值值的机制,模板字符串支持广泛的字符串处理用例。
规范
模板字符串字面量
本 PEP 引入了一个新的字符串前缀 t,用于定义模板字符串字面量。这些字面量解析为一个新类型 Template,位于标准库模块 string.templatelib 中。
以下代码创建一个 Template 实例:
from string.templatelib import Template
template = t"This is a template string."
assert isinstance(template, Template)
模板字符串字面量支持 PEP 701 的完整语法。这包括在插值中嵌套模板字符串的能力,以及使用所有有效引号('、"、''' 和 """)的能力。与其他字符串前缀一样,t 前缀必须紧跟在引号之前。与 f-字符串一样,小写 t 和大写 T 前缀都受支持。与 f-字符串一样,t-字符串不能与 u 或 b 前缀组合。
此外,f-字符串和 t-字符串不能组合,因此 ft 前缀是无效的。t-字符串 可以 与 r 前缀组合;有关更多信息,请参阅下面的原始模板字符串部分。
Template 类型
模板字符串求值为一个新的不可变类型 string.templatelib.Template 的实例。
class Template:
strings: tuple[str, ...]
"""
A non-empty tuple of the string parts of the template,
with N+1 items, where N is the number of interpolations
in the template.
"""
interpolations: tuple[Interpolation, ...]
"""
A tuple of the interpolation parts of the template.
This will be an empty tuple if there are no interpolations.
"""
def __new__(cls, *args: str | Interpolation):
"""
Create a new Template instance.
Arguments can be provided in any order.
"""
...
@property
def values(self) -> tuple[object, ...]:
"""
Return a tuple of the `value` attributes of each Interpolation
in the template.
This will be an empty tuple if there are no interpolations.
"""
...
def __iter__(self) -> Iterator[str | Interpolation]:
"""
Iterate over the string parts and interpolations in the template.
These may appear in any order. Empty strings will not be included.
"""
...
strings 和 interpolations 属性提供对字面量中的字符串部分和任何插值的访问。
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"
当开发者明确构建 Interpolation 时,他们可以选择为 expression 属性提供一个值。尽管它存储为字符串,但这 应该 是一个有效的 Python 表达式。如果没有提供值,expression 属性默认为空字符串 ("")。
我们预计 expression 属性在大多数模板处理代码中不会被使用。它提供是为了完整性以及在调试和自省中使用。有关如何处理模板字符串的更多信息,请参阅 处理模板中常见的模式 部分和 示例 部分。
conversion 属性是要使用的可选转换,可以是 r、s 和 a 之一,分别对应 repr()、str() 和 ascii() 转换。与 f-字符串一样,不支持其他转换。
name = "World"
template = t"Hello {name!r}"
assert template.interpolations[0].conversion == "r"
如果没有提供转换,conversion 为 None。
format_spec 属性是格式规范。与 f-字符串一样,这是一个任意字符串,用于定义如何呈现值。
value = 42
template = t"Value: {value:.2f}"
assert template.interpolations[0].format_spec == ".2f"
f-字符串中的格式规范本身可以包含插值。这在模板字符串中也是允许的;format_spec 被设置为急切求值的结果。
value = 42
precision = 2
template = t"Value: {value:.{precision}f}"
assert template.interpolations[0].format_spec == ".2f"
如果没有提供格式规范,format_spec 默认为空字符串 ("")。这与 Python 内置函数 format() 的 format_spec 参数匹配。
与 f-字符串不同,解释 conversion 和 format_spec 属性的方式由处理模板的代码决定。此类代码不要求使用这些属性,但如果存在,应予以尊重,并尽可能与 f-字符串的行为匹配。例如,如果使用 {value:.2f} 的模板字符串在处理时没有将值四舍五入到两位小数,那将是令人惊讶的。
Template.values 属性
Template.values 属性是访问模板中每个 Interpolation 的 value 属性的快捷方式,等效于:
@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"
# However, the strings attribute contains empty strings:
assert template.strings == ("", "", "")
模板处理代码可以根据需求和便利性选择使用 strings、interpolations、values 和 __iter__() 的任意组合。
处理模板字符串
开发者可以编写任意代码来处理模板字符串。例如,以下函数以小写字母渲染模板的静态部分,以大写字母渲染插值:
from string.templatelib import Template, Interpolation
def lower_upper(template: Template) -> str:
"""Render static parts lowercased and interpolations uppercased."""
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.__add__() 支持两个 Template 实例的连接。
name = "World"
assert isinstance(t"Hello " + t"{name}", Template)
assert (t"Hello " + t"{name}").strings == ("Hello ", "")
assert (t"Hello " + t"{name}").values[0] == "World"
也支持两个模板字符串字面量的隐式连接。
name = "World"
assert isinstance(t"Hello " t"{name}", Template)
assert (t"Hello " t"{name}").strings == ("Hello ", "")
assert (t"Hello " t"{name}").values[0] == "World"
禁止隐式和显式连接 Template 和 str。这是因为 str 应该被视为静态字符串部分还是插值存在歧义。
要组合 Template 和 str,开发者必须明确决定如何处理 str。如果 str 旨在作为静态字符串部分,则应将其包装在 Template 中。如果 str 旨在作为插值值,则应将其包装在 Interpolation 中,并传递给 Template 构造函数。例如:
name = "World"
# Treat `name` as a static string part
template = t"Hello " + Template(name)
# Treat `name` as an interpolation
template = t"Hello " + Template(Interpolation(name, "name"))
Template 和 Interpolation 的相等性
Template 和 Interpolation 实例通过对象标识 (is) 进行比较。
Template 实例旨在由模板处理代码使用,该代码可以返回字符串或任何其他类型。这些类型可以根据需要提供自己的相等语义。
不支持排序
Template 和 Interpolation 类型不支持排序。这与 Python 中所有其他支持词典排序的字符串字面量类型不同。由于插值可以包含任意值,因此它们没有自然的排序。因此,Template 和 Interpolation 类型都没有实现标准比较方法。
支持调试指定符 (=)
模板字符串中支持调试指定符 =,其行为与在 f-字符串中类似,但由于实现限制,略有不同。
特别是,t'{value=}' 被视为 t'value={value!r}'。第一个静态字符串从 "" 重写为 "value=",并且 conversion 默认为 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=!s}' 被视为 t'value={value!s}'。
如果提供了格式字符串但没有转换,则 conversion 设置为 None:t'{value=:fmt}' 被视为 t'value={value: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-字符串一样,从左到右急切求值。这意味着在处理模板字符串时,插值会立即求值,而不是延迟或包装在 lambda 中。
异常
t-字符串字面量中引发的异常与 f-字符串字面量中引发的异常相同。
没有 Template.__str__() 实现
Template 类型不提供专门的 __str__() 实现。
这是因为 Template 实例旨在由模板处理代码使用,该代码可以返回字符串或任何其他类型。没有规范的方法将 Template 转换为字符串。
Template 和 Interpolation 类型都提供了有用的 __repr__() 实现。
string.templatelib 模块
string 模块将被转换为一个包,其中包含一个新的 templatelib 子模块,其中包含 Template 和 Interpolation 类型。在本 PEP 实现之后,这个新模块可以用于相关函数,例如 convert(),或未来潜在的模板处理代码,例如 shell 脚本助手。
示例
本 PEP 这一部分的所有示例都在公共 pep750-examples git 仓库中提供了经过全面测试的参考实现。
示例:使用 t-字符串实现 f-字符串
使用 t-字符串“实现”f-字符串非常容易。也就是说,我们可以编写一个函数 f(template: Template) -> str,以与 f-字符串字面量非常相似的方式处理 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)
示例:结构化日志
结构化日志允许开发者以 JSON 等机器可读格式记录数据。使用 t-字符串,开发者可以轻松地将结构化数据与人类可读消息一起记录,只需一个日志语句。
我们提出了两种不同的方法来使用模板字符串实现结构化日志。
方法 1:自定义日志消息
Python 日志食谱 中有一小节关于如何实现结构化日志。
日志食谱建议创建一个新的“消息”类 StructuredMessage,该类使用一个简单的文本消息和一个独立的字典值进行构建。
message = StructuredMessage("user action", {
"action": "traded",
"amount": 42,
"item": "shrubs"
})
logging.info(message)
# Outputs:
# user action >>> {"action": "traded", "amount": 42, "item": "shrubs"}
StructuredMessage.__str__() 方法格式化人类可读消息 和 值,将它们组合成最终字符串。(有关完整示例,请参阅日志食谱。)
我们可以使用模板字符串实现改进版的 StructuredMessage。
import json
from string.templatelib import Interpolation, Template
from typing import Mapping
class TemplateMessage:
def __init__(self, template: Template) -> None:
self.template = template
@property
def message(self) -> str:
# Use the f() function from the previous example
return f(self.template)
@property
def values(self) -> Mapping[str, object]:
return {
item.expression: item.value
for item in self.template
if isinstance(item, Interpolation)
}
def __str__(self) -> str:
return f"{self.message} >>> {json.dumps(self.values)}"
_ = TemplateMessage # optional, to improve readability
action, amount, item = "traded", 42, "shrubs"
logging.info(_(t"User {action}: {amount:.2f} {item}"))
# Outputs:
# User traded: 42.00 shrubs >>> {"action": "traded", "amount": 42, "item": "shrubs"}
模板字符串为我们提供了一种更优雅的方式来定义自定义消息类。使用模板字符串,开发者不再需要确保其格式字符串和值字典保持同步;一个模板字符串字面量就足够了。TemplateMessage 实现可以自动从 Interpolation.expression 和 Interpolation.value 属性中分别提取结构化键和值。
方法 2:自定义格式化器
自定义消息是结构化日志的一种合理方法,但可能有点笨拙。要使用它们,开发者必须将其编写的每条日志消息包装在一个自定义类中。这很容易忘记。
另一种方法是定义自定义 logging.Formatter 类。这种方法更灵活,并且允许对最终输出进行更多控制。特别是,可以将单个模板字符串以多种格式(人类可读和 JSON)输出到单独的日志流中。
我们定义了两个简单的格式化器,一个用于人类可读输出的 MessageFormatter 和一个用于 JSON 输出的 ValuesFormatter。
import json
from logging import Formatter, LogRecord
from string.templatelib import Interpolation, Template
from typing import Any, Mapping
class MessageFormatter(Formatter):
def message(self, template: Template) -> str:
# Use the f() function from the previous example
return f(template)
def format(self, record: LogRecord) -> str:
msg = record.msg
if not isinstance(msg, Template):
return super().format(record)
return self.message(msg)
class ValuesFormatter(Formatter):
def values(self, template: Template) -> Mapping[str, Any]:
return {
item.expression: item.value
for item in template
if isinstance(item, Interpolation)
}
def format(self, record: LogRecord) -> str:
msg = record.msg
if not isinstance(msg, Template):
return super().format(record)
return json.dumps(self.values(msg))
然后我们可以在配置日志记录器时使用这些格式化器。
import logging
import sys
logger = logging.getLogger(__name__)
message_handler = logging.StreamHandler(sys.stdout)
message_handler.setFormatter(MessageFormatter())
logger.addHandler(message_handler)
values_handler = logging.StreamHandler(sys.stderr)
values_handler.setFormatter(ValuesFormatter())
logger.addHandler(values_handler)
action, amount, item = "traded", 42, "shrubs"
logger.info(t"User {action}: {amount:.2f} {item}")
# Outputs to sys.stdout:
# User traded: 42.00 shrubs
# At the same time, outputs to sys.stderr:
# {"action": "traded", "amount": 42, "item": "shrubs"}
这种方法比自定义消息方法在结构化日志方面有一些优势。
- 开发者可以直接记录 t-字符串,而无需将其包装在自定义类中。
- 人类可读和结构化输出可以发送到单独的日志流。这对于独立处理结构化数据和人类可读数据的日志聚合系统很有用。
示例:HTML 模板
本 PEP 包含几个简短的 HTML 模板示例。事实证明,动机 部分(以及本 PEP 中的其他一些地方)提到的“假设性” html() 函数确实存在,并且可在 pep750-examples 仓库 中找到。如果您正在考虑使用模板字符串解析复杂的语法,我们希望它对您有所帮助。
向后兼容性
与 f-字符串一样,使用模板字符串将导致与以前版本在语法上的不兼容。
安全隐患
使用模板字符串,就插值而言,其安全隐患如下:
- 作用域查找与 f-字符串相同(词法作用域)。这种模型已被证明在实践中运行良好。
- 处理
Template实例的代码可以确保任何插值都以安全的方式处理,包括遵守它们出现的上下文。
如何教授
模板字符串有几个受众:
- 使用模板字符串和处理函数的开发者
- 模板处理代码的作者
- 使用模板字符串构建有趣机制的框架作者
我们希望教学开发者将是直接的。乍一看,模板字符串就像 f-字符串。它们的语法很熟悉,作用域规则也保持不变。
开发者首先必须学习的是,模板字符串字面量不会求值为字符串;相反,它们求值为一个新类型 Template。这是一个简单的类型,旨在由模板处理代码使用。只有当开发者调用处理函数时,他们才能得到想要的结果:通常是一个字符串,尽管处理代码当然可以返回任何任意类型。
开发者还需要了解模板字符串与其他字符串格式化方法(如 f-字符串和 str.format())的关系。他们需要决定何时使用每种方法。如果只需要一个简单的字符串,并且没有安全隐患,f-字符串可能是最佳选择。对于大多数使用格式字符串的情况,它可以用一个包装模板字符串创建的函数替换。在从用户输入、文件系统或数据库获取格式字符串的情况下,如果需要,可以编写代码将其转换为 Template 实例。
因为开发者将了解到 t-字符串几乎总是与处理函数结合使用,所以他们不一定需要了解 Template 类型的细节。与描述符和装饰器一样,我们预计使用 t-字符串的开发者将比编写 t-字符串处理函数的开发者多得多。
随着时间的推移,少数更高级的开发者 将 希望编写自己的模板处理代码。编写处理代码通常需要从形式语法角度思考。开发者需要学习如何使用 Template 实例的 strings 和 interpolation 属性,以及如何以上下文敏感的方式处理插值。更复杂的语法可能需要解析为中间表示,如抽象语法树 (AST)。出色的模板处理代码将在适当的时候处理格式说明符和转换。编写生产级模板处理代码——例如,支持 HTML 模板——可能是一项庞大的工作。
我们预计模板字符串将为框架作者提供一个强大的新工具。虽然模板字符串的功能与现有工具(如模板引擎)重叠,但 t-字符串将该逻辑移入语言本身。将 Python 的全部能力和通用性应用于字符串处理任务为框架作者开辟了新的可能性。
为何采用另一种模板方法?
Python 世界已经拥有成熟且广泛采用的模板语言,例如 Jinja。为什么还要为创建新的模板系统构建支持呢?
在模板是设计师或甚至用户(例如在 CMS 中)创建的定制内容,而不是开发者软件的一部分的情况下,Jinja 等项目仍然是必需的。
前端开发趋势将模板视为软件的一部分,由开发者编写。他们想要现代语言特性和良好的工具体验。PEP 750 设想的 DSL 中,非静态部分是 Python:相同的范围规则、类型、表达式语法等。
处理模板中常见的模式
结构化模式匹配
对于许多模板函数实现来说,通过结构化模式匹配迭代 Template 是预期的最佳实践。
from string.templatelib import Template, Interpolation
def process(template: Template) -> Any:
for item in template:
match item:
case str() as s:
... # handle each string part
case Interpolation() as interpolation:
... # handle each interpolation
处理代码也可能通常对 Interpolation 类型的属性进行子匹配。
match arg:
case Interpolation(int()):
... # handle interpolations with integer values
case Interpolation(value=str() as s):
... # handle interpolations with string values
# etc.
记忆化
模板函数可以高效地处理模板的静态和动态部分。Template 对象的结构允许有效的记忆化。
strings = template.strings # Static string parts
values = template.values # Dynamic interpolated values
这种分离使得已处理的静态部分可以缓存,而动态部分可以按需插入。模板处理代码的作者可以使用静态 strings 作为缓存键,当重复使用相似模板时,可以显著提高性能。
解析为中间表示
处理模板的代码可以将模板字符串解析为中间表示,例如 AST。我们预计许多模板处理库将使用这种方法。
例如,我们的理论 html() 函数(参见动机部分)可以返回一个在同一包中定义的 HTML Element,而不是返回 str。
@dataclass(frozen=True)
class Element:
tag: str
attributes: Mapping[str, str | bool]
children: Sequence[str | Element]
def __str__(self) -> str:
...
def html(template: Template) -> Element:
...
调用 str(element) 将会渲染 HTML,但在此期间,Element 可以通过多种方式进行操作。
插值的上下文敏感处理
继续我们的假设 html() 函数,它可以是上下文敏感的。插值可以根据它们在模板中出现的位置进行不同的处理。
例如,我们的 html() 函数可以支持多种插值类型。
attributes = {"id": "main"}
attribute_value = "shrubbery"
content = "hello"
template = t"<div {attributes} data-value={attribute_value}>{content}</div>"
element = html(template)
assert str(element) == '<div id="main" data-value="shrubbery">hello</div>'
由于 {attributes} 插值出现在 HTML 标签的上下文中,并且没有对应的属性名称,因此它被视为一个属性字典。{attribute_value} 插值被视为一个简单的字符串值,并在包含到最终字符串之前被引用。 {content} 插值被视为潜在不安全的内容,并在包含到最终字符串之前被转义。
嵌套模板字符串
更进一步,使用我们的 html() 函数,我们可以支持嵌套模板字符串。这将允许从更简单的模板构建更复杂的 HTML 结构。
name = "World"
content = html(t"<p>Hello {name}</p>")
template = t"<div>{content}</div>"
element = html(template)
assert str(element) == '<div><p>Hello World</p></div>'
由于 {content} 插值是一个 Element 实例,因此在包含到最终字符串之前无需对其进行转义。
我们可以想象一个很好的简化:如果 html() 函数被传入一个 Template 实例,它可以通过递归调用自身处理嵌套模板来自动将其转换为 Element。
我们预计模板的嵌套和组合将是模板处理代码中的常见模式,并且在适当的情况下,优先于简单的字符串连接。
延迟求值的方法
与 f-字符串一样,t-字符串字面量中的插值会急切求值。然而,在某些情况下,延迟求值可能更可取。
如果单个插值求值代价昂贵,则可以在模板字符串字面量中将其显式包装在 lambda 中。
name = "World"
template = t"Hello {(lambda: name)}"
assert callable(template.interpolations[0].value)
assert template.interpolations[0].value() == "World"
当然,这假设模板处理代码预期并处理可调用插值值。(我们也可以想象支持迭代器、可等待对象等。)这不是 PEP 的要求,但它是模板处理代码中的常见模式。
总的来说,我们希望社区能为模板字符串中插值的延迟求值制定最佳实践,并且在有意义的情况下,常用库将在其模板处理代码中提供对可调用或可等待值的支持。
异步求值的方法
与延迟求值密切相关的是异步求值。
与 f-字符串一样,await 关键字允许在插值中使用。
async def example():
async def get_name() -> str:
await asyncio.sleep(1)
return "Sleepy"
template = t"Hello {await get_name()}"
# Use the f() function from the f-string example, above
assert f(template) == "Hello Sleepy"
更复杂的模板处理代码可以利用这一点在插值中执行异步操作。例如,一个“智能”处理函数可以预期插值是一个可等待对象,并在处理模板字符串之前等待它。
async def example():
async def get_name() -> str:
await asyncio.sleep(1)
return "Sleepy"
template = t"Hello {get_name}"
assert await async_f(template) == "Hello Sleepy"
这假设 async_f() 中的模板处理代码是异步的,并且能够 await 插值的值。
模板复用的方法
如果开发者希望多次使用不同的值重用模板字符串,他们可以编写一个函数来返回一个 Template 实例。
def reusable(name: str, question: str) -> Template:
return t"Hello {name}, {question}?"
template = reusable("friend", "how are you")
template = reusable("King Arthur", "what is your quest")
这当然与 f-字符串的重用方式没有什么不同。
与格式字符串的关系
古老的 str.format() 方法接受格式字符串,这些格式字符串以后可以用于格式化值。
alas_fmt = "We're all out of {cheese}."
assert alas_fmt.format(cheese="Red Leicester") == "We're all out of Red Leicester."
如果眯着眼睛看,可以将格式字符串看作一种函数定义。str.format() 的 调用 可以看作一种函数调用。t-字符串的等价物是简单地定义一个标准 Python 函数,该函数返回一个 Template 实例。
def make_template(*, cheese: str) -> Template:
return t"We're all out of {cheese}."
template = make_template(cheese="Red Leicester")
# Using the f() function from the f-string example, above
assert f(template) == "We're all out of Red Leicester."
make_template() 函数本身可以被视为与格式字符串类比。make_template() 的调用与 str.format() 的调用类比。
当然,从文件系统或数据库等外部源加载格式字符串是很常见的。幸运的是,由于 Template 和 Interpolation 是简单的 Python 类型,因此可以编写一个函数,接受旧式格式字符串并返回等效的 Template 实例。
def from_format(fmt: str, /, *args: object, **kwargs: object) -> Template:
"""Parse `fmt` and return a `Template` instance."""
...
# Load this from a file, database, etc.
fmt = "We're all out of {cheese}."
template = from_format(fmt, cheese="Red Leicester")
# Using the f() function from the f-string example, above
assert f(template) == "We're all out of Red Leicester."
这是一种强大的模式,允许开发者在以前可能使用格式字符串的地方使用模板字符串。示例仓库中提供了 from_format() 的完整实现,它支持格式字符串的完整语法。
参考实现
PEP 750 的 CPython 实现可用。
还有一个公共仓库包含围绕参考实现的示例和测试。如果您对玩转模板字符串感兴趣,这个仓库是一个很好的起点。
被拒绝的想法
本 PEP 经历了几个重要的修订。此外,在 PEP 501 的修订和 Discourse 讨论 中都考虑了相当多有趣的想法。
我们尝试记录那些被考虑和拒绝的最重要的想法。
任意字符串字面量前缀
受 JavaScript 标记模板字面量 的启发,本 PEP 的早期版本允许在字面量字符串前使用任意的“标记”前缀。
my_tag'Hello {name}'
该前缀是一个特殊的名为“标记函数”的可调用对象。标记函数接收模板字符串的部分作为参数列表。然后它们可以处理字符串并返回任意值。
def my_tag(*args: str | Interpolation) -> Any:
...
这种方法因以下几个原因被拒绝:
- 它被认为过于复杂,无法实现通用性。JavaScript 允许任意表达式在模板字符串之前,这在 Python 中实现是一个重大挑战。
- 它排除了未来引入新字符串前缀的可能性。
- 它似乎不必要地污染了命名空间。
选择单个 t 前缀作为一种更简单、更 Pythonic 的方法,并且更符合模板字符串作为 f-字符串泛化的角色。
插值的延迟求值
本 PEP 的早期版本提出插值应延迟求值。所有插值都被“包装”在隐式 lambda 中。插值没有急切求值的 value 属性,而是有一个 getvalue() 方法,该方法将解析插值的值。
class Interpolation:
...
_value: Callable[[], object]
def getvalue(self) -> object:
return self._value()
这被拒绝的原因有几个:
- 绝大多数模板字符串用例自然需要立即求值。
- 延迟求值将与 f-字符串的行为显著偏离。
- 隐式 lambda 包装导致类型提示和静态分析困难。
最重要的是,在许多需要延迟求值的情况下,存在可行(尽管不完美)的替代隐式 lambda 包装的方法。有关更多信息,请参阅上面的延迟求值方法一节。
尽管本 PEP 拒绝了延迟求值,但我们希望社区继续探索这个想法。
将 Template 和 Interpolation 转换为协议
本 PEP 的早期版本曾提议将 Template 和 Interpolation 类型作为运行时可检查的协议而非类。
最终,我们觉得使用类更直接。
为 Template 和 Interpolation 重写 __eq__ 和 __hash__
本 PEP 的早期版本曾提议 Template 和 Interpolation 类型应拥有自己的 __eq__ 和 __hash__ 实现。
如果 Templates 的 strings 和 interpolations 相等,则认为它们相等;如果 Interpolations 的 value、expression、conversion 和 format_spec 相等,则认为它们相等。插值哈希类似于元组哈希:当且仅当其 value 可哈希时,Interpolation 才是可哈希的。
这被拒绝了,因为如此定义的 Template.__hash__ 在模板处理代码中作为缓存键没有用处;我们担心这会使开发者感到困惑。
通过放弃这些 __eq__ 和 __hash__ 的实现,我们失去了编写如下断言的能力:
name = "World"
assert t"Hello " + t"{name}" == t"Hello {name}"
由于 Template 实例旨在由后续代码快速处理,我们认为这些断言的实用性有限。
一个额外的 Decoded 类型
本 PEP 的早期版本提出了一种附加类型 Decoded,用于表示模板字符串的“静态字符串”部分。这种类型派生自 str,并具有一个额外的 raw 属性,提供字符串的原始文本。我们拒绝了这种方法,转而采用使用普通 str 并允许组合 r 和 t 前缀的更简单方法。
Template 和 Interpolation 的最终归宿
本 PEP 的先前版本提议将 Template 和 Interpolation 类型放置在:types、collections、collections.abc,甚至一个新的顶级模块 templatelib 中。最终决定将它们放置在 string.templatelib 中。
启用原始模板字面量的完全重建
本 PEP 的早期版本曾试图通过 Template 实例完全重构原始模板字符串的文本。这被认为过于复杂而被拒绝。模板字面量源代码与底层 AST 之间的映射不是一对一的,并且在往返原始源代码文本方面存在一些限制。
首先,如果未提供,Interpolation.format_spec 默认为 ""。
value = 42
template1 = t"{value}"
template2 = t"{value:}"
assert template1.interpolations[0].format_spec == ""
assert template2.interpolations[0].format_spec == ""
其次,调试指定符 = 被视为特殊情况,并在 AST 创建之前处理。因此,无法区分 t"{value=}" 和 t"value={value!r}"。
value = 42
template1 = t"{value=}"
template2 = t"value={value!r}"
assert template1.strings[0] == "value="
assert template1.interpolations[0].expression == "value"
assert template1.interpolations[0].conversion == "r"
assert template2.strings[0] == "value="
assert template2.interpolations[0].expression == "value"
assert template2.interpolations[0].conversion == "r"
最后,f-字符串中的格式说明符允许任意嵌套。在本 PEP 和参考实现中,该说明符会被急切求值,以设置 Interpolation 中的 format_spec,从而丢失原始表达式。例如:
value = 42
precision = 2
template1 = t"{value:.2f}"
template2 = t"{value:.{precision}f}"
assert template1.interpolations[0].format_spec == ".2f"
assert template2.interpolations[0].format_spec == ".2f"
我们不认为这些限制在实践中会成为一个重大问题。需要获取原始模板字符串字面量的开发者总是可以使用 inspect.getsource() 或类似工具。
禁止模板连接
本 PEP 的早期版本曾提议 Template 实例不应支持连接。这被拒绝了,转而允许连接多个 Template 实例。
有合理的论据支持拒绝一种或所有形式的连接:即,它切断了一类潜在的错误,特别是当人们认为模板字符串通常包含复杂的语法,而连接并不总是具有相同的含义(或任何含义)时。
此外,本 PEP 的早期版本提出了更接近 JavaScript 标记模板字面量的语法,其中任意可调用对象可以用作字符串字面量的前缀。无法保证该可调用对象将返回支持连接的类型。
最终,我们认为一种新的字符串类型 不 支持连接给开发者带来的意外,可能大于支持它所造成的理论危害。
虽然本 PEP 支持两个 Template 的连接,但不支持 Template 和 str 的连接。这是因为 str 应该被视为静态字符串还是插值存在歧义。开发者必须将 str 包装在一个 Template 实例中,然后才能将其与另一个 Template 连接,如上所述。
我们预计使用模板字符串的代码将更常见地通过嵌套和组合而不是连接来构建更大的模板。
任意转换值
Python 只允许 r、s 或 a 作为可能的转换类型值。尝试赋值不同的值将导致 SyntaxError。
理论上,模板函数可以选择处理其他转换类型。但本 PEP 严格遵循 PEP 701。对允许值的任何更改都应在单独的 PEP 中进行。
从 Interpolation 中移除 conversion
在起草本 PEP 期间,我们曾考虑从 Interpolation 中移除 conversion 属性,并指定应在设置 Interpolation.value 之前急切地执行转换。
这样做是为了简化编写模板处理代码的工作。conversion 属性的扩展性有限(它被类型化为 Literal["r", "s", "a"] | None)。尚不清楚它是否为模板字符串增加了显著的价值或灵活性,这些价值或灵活性无法通过自定义格式说明符更好地实现。与格式说明符不同,没有与 Python 内置函数 format() 等效的函数。(相反,我们在示例部分包含了一个 convert() 的示例实现。)
最终,我们决定在 Interpolation 类型中保留 conversion 属性,以保持与 f-字符串的兼容性并允许未来扩展。
替代插值符号
在本 PEP 的早期阶段,我们曾考虑允许在模板字符串中使用替代符号进行插值。例如,我们曾考虑允许 ${name} 作为 {name} 的替代,认为它可能对国际化或其他目的有用。有关更多信息,请参阅 Discourse 讨论。
这被拒绝了,转而使 t-字符串语法尽可能接近 f-字符串语法。
Template 的替代布局
在本 PEP 的开发过程中,我们考虑了 Template 类型的几种替代布局。许多都集中在一个包含字符串和插值的单一 args 元组上。变体包括:
args是一个tuple[str | Interpolation, ...],承诺其第一个和最后一个项是字符串,并且字符串和插值总是交替出现。这意味着args总是非空的,并且空字符串将插入相邻的插值之间。这被拒绝了,因为交替无法被类型系统捕获,也不是我们希望做出的保证。args仍然是tuple[str | Interpolation, ...],但不支持交错。因此,空字符串没有添加到序列中。不再可能使用args[::2]获取静态字符串;相反,必须使用实例检查或结构化模式匹配来区分字符串和插值。这种方法被拒绝,因为它提供的未来性能优化机会较少。args被类型化为Sequence[tuple[str, Interpolation | None]]。每个静态字符串都与其相邻的插值配对。最终的字符串部分没有对应的插值。这被拒绝了,因为它过于复杂。
描述模板“种类”的机制
如果 t-字符串变得流行,那么有一种方法来描述模板字符串中内容的“种类”可能会很有用:“sql”、“html”、“css”等。这可以在 Linter、格式化器、类型检查器和 IDE 等工具中启用强大的新功能。(例如,想象一下 black 在 t-字符串中格式化 HTML,或者 mypy 检查给定属性对于 HTML 标签是否有效。)虽然令人兴奋,但本 PEP 没有提出任何具体的机制。我们希望随着时间的推移,社区将为此目的制定约定。
二进制模板字符串
t-字符串与字节 (tb) 的组合被认为超出了本 PEP 的范围。然而,与 f-字符串不同,t-字符串与字节组合没有根本原因。未来可以在 PEP 中考虑支持。
致谢
感谢 Ryan Morshead 在模板字符串思想发展过程中的贡献。特别感谢 Dropbox 的 pyxl 多年前处理过类似的想法。Andrea Giammarchi 为本 PEP 的早期草稿提供了深思熟虑的反馈。最后,感谢 Joachim Viide 在 tagged 库 上的开创性工作。Tagged 不仅是模板字符串的前身,更是整个工作通过 GitHub issue 评论开始的地方!
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0750.rst