PEP 501 – 通用模板字面量字符串
- 作者:
- Alyssa Coghlan <ncoghlan at gmail.com>,Nick Humrich <nick at humrich.us>
- 讨论邮件列表:
- Discourse 讨论主题
- 状态:
- 草案
- 类型:
- 标准跟踪
- 依赖:
- 701
- 创建日期:
- 2015年8月8日
- Python 版本:
- 3.12
- 历史记录:
- 2015年8月8日,2015年9月5日,2023年3月9日
摘要
尽管 Python f-字符串 易于使用且优雅,但当用于构建 shell 命令、SQL 查询、HTML 片段等时,它们容易受到注入攻击(例如,os.system(f"echo {message_from_user}")
)。本 PEP 引入了模板字面量字符串(或“t-字符串”),其语法和语义类似于 f-字符串,但渲染被延迟到对它们调用 format()
或其他模板渲染函数时。这将允许标准库调用、辅助函数和第三方工具安全且智能地对输入执行适当的转义和其他字符串处理,同时保留 f-字符串的可用性和便利性。
与其他 PEP 的关系
本 PEP 受 PEP 498 中首次实现的 f-字符串语法以及 PEP 701 中正式化的语法的启发,并在此基础上构建。
本 PEP 通过引入一种安全的方式将运行时值动态插入安全敏感字符串中,补充了 PEP 675 中添加到 Python 正式类型系统中的字面量字符串类型支持。
本 PEP 与 PEP 750 中的标记字符串提案的一些方面存在竞争(最值得注意的是模板渲染是否表示为 render(t"template literal")
或 render"template literal"
),但也共享许多共同特征(在 PEP 750 发布后,本 PEP 已更新,其中包含受标记字符串提案启发的几个新更改)。
本 PEP 并不建议替代 PEP 292 用于用户界面国际化用例(但确实指出了未来针对该用例的语法增强功能的潜力,这些增强功能将受益于本 PEP 和 PEP 750 引入的编译器支持的值插值机制)。
动机
PEP 498 添加了对字符串插值的新语法支持,该语法对编译器是透明的,允许插值操作中的名称引用完全访问包含命名空间(与任何其他表达式一样),而不是仅限于显式名称引用。这些在 PEP(以及其他地方)中被称为“f-字符串”(“格式化字符串”的助记符)。
自 PEP 498 被接受以来,f-字符串已得到广泛认可并非常流行。随着 PEP 701 中正式化的语法,f-字符串变得更加有用和灵活。虽然 f-字符串很棒,但急切渲染也有其局限性。例如,f-字符串的急切性使得如下代码不幸地变得合理
os.system(f"echo {message_from_user}")
这种代码表面上很优雅,但如果插值值 message_from_user
实际上是由不受信任的用户提供的,则会带来重大问题:它为一种代码注入攻击打开了大门,其中提供的用户数据在传递给 os.system
调用之前未正确转义。
虽然 PEP 675 中引入的 LiteralString
类型注释意味着类型检查器能够针对这种不安全的函数使用报告类型错误,但这些错误无助于简化编写使用更安全替代方案(例如 subprocess.run()
)的代码。
为了解决该问题(以及其他一些问题),本 PEP 提出了补充引入“t-字符串”(“模板字面量字符串”的助记符),其中 format(t"Message with {data}")
将产生与 f"Message with {data}"
相同的结果,但模板字面量实例可以传递给其他模板渲染函数,这些函数以不同的方式处理模板内容。
提案
专用的模板字面量语法
本 PEP 提出了一种新的字符串前缀,该前缀声明字符串是模板字面量而不是普通字符串
template = t"Substitute {names:>{field_width}} and {expressions()!r} at runtime"
这将有效地解释为
template = TemplateLiteral(
r"Substitute {names:>{field_width}} and {expressions()} at runtime",
TemplateLiteralText(r"Substitute "),
TemplateLiteralField("names", names, f">{field_width}", ""),
TemplateLiteralText(r" and "),
TemplateLiteralField("expressions()", expressions(), f"", "r"),
)
(注意:这是一个说明性的示例实现。 types.TemplateLiteral
的确切编译时构造语法被认为是 PEP 未指定的实现细节。特别是,编译器可能会绕过默认构造函数的运行时逻辑,该逻辑检测连续的文本段并将它们合并为单个文本段,以及检查所有提供的参数的运行时类型)。
types.TemplateLiteral
上的 __format__
方法将实现以下 str.format()
启发的语义
>>> import datetime
>>> name = 'Jane'
>>> age = 50
>>> anniversary = datetime.date(1991, 10, 12)
>>> format(t'My name is {name}, my age next year is {age+1}, my anniversary is {anniversary:%A, %B %d, %Y}.')
'My name is Jane, my age next year is 51, my anniversary is Saturday, October 12, 1991.'
>>> format(t'She said her name is {name!r}.')
"She said her name is 'Jane'."
模板字面量的语法将基于 PEP 701,并在很大程度上使用相同的语法来表示模板的字符串部分。除了使用不同的前缀之外,另一个语法更改是在转换说明符的定义和处理方面,既允许 !()
作为标准转换说明符来请求在渲染时评估字段,也允许自定义渲染器定义自定义转换说明符。
本 PEP 并不建议删除或弃用任何现有的字符串格式化机制,因为当格式化不在应用程序源代码中直接存在的字符串时,这些机制仍然很有价值。
延迟字段评估转换说明符
除了对 a
、r
和 s
转换说明符的现有支持之外,str.format()
、str.format_map()
和 string.Formatter
将更新为接受 ()
作为转换说明符,这意味着“调用插值值”。
为了支持在自定义模板渲染函数中应用标准转换说明符,将添加一个新的 operator.convert_field()
函数。
内置函数 format()
的签名和行为也将更新,以接受转换说明符作为第三个可选参数。如果提供了非空的转换说明符,则在查找 __format__
方法之前,将使用 operator.convert_field()
转换该值。
自定义转换说明符
为了允许以一种仍然允许使用默认渲染器格式化模板的方式将其他特定于字段的指令传递给自定义渲染函数,转换说明符字段将允许包含第二个 !
字符。
operator.convert_field()
和 format()
(以及因此默认的 TemplateLiteral.render
模板渲染方法)将忽略该字符和转换说明符字段中的任何后续文本。
str.format()
、str.format_map()
和 string.Formatter
也将更新为接受(并忽略)自定义转换说明符。
用于 POSIX shell 命令的模板渲染器
作为延迟渲染支持的好处的实用演示,以及作为其本身的一个有价值的功能,一个新的 sh
模板渲染器将被添加到 shlex
模块中。此渲染器将生成字符串,其中所有插值字段都使用 shlex.quote()
进行转义。
subprocess.Popen
API(以及依赖它的更高级别的 API,例如 subprocess.run()
)将更新为接受插值模板并根据新的 shlex.sh
渲染器处理它们。
背景
此 PEP 最初被提议作为 PEP 498 的竞争对手。在明确表明急切渲染提案获得更多立即支持后,它随后处于延迟状态数年,等待进一步了解 PEP 498 只支持急切渲染而无需额外支持延迟渲染的复杂性的更简单方法。
从那时起,f-字符串变得非常流行,并且引入了 PEP 701 来整理其语法和语义中的一些粗糙边缘和限制。模板字面量提案在 2023 年进行了更新,以反映当前对 f-字符串的了解以及 PEP 701 的改进。
2024 年,发布了 PEP 750,提议了一种用于自定义标记字符串前缀的通用机制,而不是此 PEP 中更窄的模板字面量提案。此 PEP 再次更新,既包含受标记字符串提案启发的新的想法,又描述了此 PEP 中更窄的模板字面量语法提案相对于更通用的标记字符串提案的感知优势。
与 f-字符串的区别总结
f-字符串和 t-字符串之间的主要区别是
t
(模板字面量)前缀表示延迟渲染,但在其他方面很大程度上使用了与格式化字符串相同的语法和语义- 模板字面量在运行时可用作一种新类型的对象(
types.TemplateLiteral
) - 格式化字符串使用的默认渲染通过调用
format(template)
在模板字面量对象上调用,而不是在编译代码中隐式完成 - 与 f-字符串(在其中转换说明符在编译器中直接处理)不同,t-字符串转换说明符在渲染时由渲染函数处理
- 新的
!()
转换说明符表示字段表达式是一个可调用对象,在使用默认的format()
渲染函数时应调用它。此说明符特别未添加到 f-字符串中(因为在那里没有意义)。 - 在 t-字符串转换说明符中允许使用第二个
!
(任何后续文本都被忽略),作为一种允许自定义模板渲染函数接受自定义转换说明符而不会破坏默认的TemplateLiteral.render()
渲染方法的方法。此功能特别未添加到 f-字符串中(因为在那里没有意义)。 - 虽然 f-字符串
f"Message {here}"
在语义上等效于format(t"Message {here}")
,但 f-字符串将继续在编译器中直接支持,因此避免了实际使用 t-字符串所需的延迟渲染机制的运行时开销
与标记字符串的区别总结
当首次提出标记字符串时,除了渲染函数调用是否写为 render(t"template literal")
还是 render"template literal"
之间的表面语法差异之外,还有几个与 PEP 501 中的提案存在显着差异。
在最初的 PEP 750 讨论过程中,许多差异都被消除了,要么是 PEP 501 采用 PEP 750 提案的这一方面(例如延迟应用转换说明符),要么是 PEP 750 更改为保留 PEP 501 提案的某些方面(例如定义一个专用的类型来保存模板段,而不是将其表示为简单的序列)。
主要剩余的重大差异是,此 PEP 认为仅添加 t-字符串前缀足以提供 PEP 750 中描述的所有所需好处。扩展到广义的“标记字符串”语法没有必要,并且会导致其他可以避免的问题。
这两个 PEP 在处理模板字段的延迟评估方面也存在差异。
虽然这两个提案确实存在其他差异,但这些差异更多是表面的而不是实质性的。特别是
- 此 PEP 为结构类型协议提出了不同的名称
- 此 PEP 为具体实现类型提出了特定的名称
- 此 PEP 为具体实现类型的提议 API 提出了确切的细节(包括串联和重复支持,这些都不属于结构类型协议)
- 此 PEP 提议更改现有的
format()
内置函数,使其可以直接用作模板字段渲染器
这两个 PEP 在如何为延迟渲染支持进行论证方面也存在差异。此 PEP 更侧重于使用模板字面量允许 f-字符串处理中的“插值”和“渲染”步骤在时间上分离的具体实现概念,然后利用这一点来降低与 f-字符串误用相关的潜在代码注入风险。PEP 750 更侧重于本机模板支持允许通过现有的基于字符串的模板方法难以或不可能实现的行为的方式。与上面提到的表面差异一样,这更多是风格上的差异而不是实质上的差异。
基本原理
f-字符串(PEP 498)使使用对 Python 词法命名空间语义的完全访问权限将值插值到字符串中变得更加简单,但这样做是以创建一种情况为代价的,在这种情况下,将值插值到诸如 SQL 查询、shell 命令和 HTML 模板之类的敏感目标时,在不考虑代码注入攻击的情况下处理时将享受更简洁的语法,而不是在正确处理时享受的语法。
此 PEP 建议提供将模板字面量的实际渲染延迟到格式化字符串的其 __format__
方法的选项,允许通过将模板作为一等对象传递来使用其他模板渲染器。
虽然在技术细节上差异很大,但此 PEP 中提出的 types.TemplateLiteral
接口在概念上与 C# 6.0 中引入的本机插值支持的基础 FormattableString
类型以及 ES6 中引入的JavaScript 模板字面量非常相似。
虽然不是开发该提案的最初动机,但 PEP 750 中描述的定义领域特定语言的许多好处也适用于此 PEP(包括基于已声明模板变量和渲染函数参数的类型规范在代码编辑器中进行每个 DSL 语义突出显示的潜力)。
规范
此 PEP 提议一个新的 t
字符串前缀,这将导致创建一个新类型 types.TemplateLiteral
的实例。
模板字面量是 Unicode 字符串(不允许使用字节字面量),并且字符串字面量连接按正常方式操作,整个组合字面量形成模板字面量。
模板字符串会被解析成文字、表达式、格式说明符和转换说明符,其描述方式与 PEP 498 和 PEP 701 中 f-string 的描述相同。转换说明符的语法被放宽,允许使用任意字符串(不包括包含 {
、}
或 :
的字符串),而不是仅限于有效的 Python 标识符。
然而,这些组件并没有直接渲染成格式化字符串,而是被组织成新类型的实例,并具有以下行为
class TemplateLiteralText(str):
# This is a renamed and extended version of the DecodedConcrete type in PEP 750
# Real type would be implemented in C, this is an API compatible Python equivalent
_raw: str
def __new__(cls, raw: str):
decoded = raw.encode("utf-8").decode("unicode-escape")
if decoded == raw:
decoded = raw
text = super().__new__(cls, decoded)
text._raw = raw
return text
@staticmethod
def merge(text_segments:Sequence[TemplateLiteralText]) -> TemplateLiteralText:
if len(text_segments) == 1:
return text_segments[0]
return TemplateLiteralText("".join(t._raw for t in text_segments))
@property
def raw(self) -> str:
return self._raw
def __repr__(self) -> str:
return f"{type(self).__name__}(r{self._raw!r})"
def __add__(self, other:Any) -> TemplateLiteralText|NotImplemented:
if isinstance(other, TemplateLiteralText):
return TemplateLiteralText(self._raw + other._raw)
return NotImplemented
def __mul__(self, other:Any) -> TemplateLiteralText|NotImplemented:
try:
factor = operator.index(other)
except TypeError:
return NotImplemented
return TemplateLiteralText(self._raw * factor)
__rmul__ = __mul__
class TemplateLiteralField(NamedTuple):
# This is mostly a renamed version of the InterpolationConcrete type in PEP 750
# However:
# - value is eagerly evaluated (values were all originally lazy in PEP 750)
# - conversion specifiers are allowed to be arbitrary strings
# - order of fields is adjusted so the text form is the first field and the
# remaining parameters match the updated signature of the `*format` builtin
# Real type would be implemented in C, this is an API compatible Python equivalent
expr: str
value: Any
format_spec: str | None = None
conversion_spec: str | None = None
def __repr__(self) -> str:
return (f"{type(self).__name__}({self.expr}, {self.value!r}, "
f"{self.format_spec!r}, {self.conversion_spec!r})")
def __str__(self) -> str:
return format(self.value, self.format_spec, self.conversion_spec)
def __format__(self, format_override) -> str:
if format_override:
format_spec = format_override
else:
format_spec = self.format_spec
return format(self.value, format_spec, self.conversion_spec)
class TemplateLiteral:
# This type corresponds to the TemplateConcrete type in PEP 750
# Real type would be implemented in C, this is an API compatible Python equivalent
_raw_template: str
_segments = tuple[TemplateLiteralText|TemplateLiteralField]
def __new__(cls, raw_template:str, *segments:TemplateLiteralText|TemplateLiteralField):
self = super().__new__(cls)
self._raw_template = raw_template
# Check if there are any adjacent text segments that need merging
# or any empty text segments that need discarding
type_err = "Template literal segments must be template literal text or field instances"
text_expected = True
needs_merge = False
for segment in segments:
match segment:
case TemplateLiteralText():
if not text_expected or not segment:
needs_merge = True
break
text_expected = False
case TemplateLiteralField():
text_expected = True
case _:
raise TypeError(type_err)
if not needs_merge:
# Match loop above will have checked all segments
self._segments = segments
return self
# Merge consecutive runs of text fields and drop any empty text fields
merged_segments:list[TemplateLiteralText|TemplateLiteralField] = []
pending_merge:list[TemplateLiteralText] = []
for segment in segments:
match segment:
case TemplateLiteralText() as text_segment:
if text_segment:
pending_merge.append(text_segment)
case TemplateLiteralField():
if pending_merge:
merged_segments.append(TemplateLiteralText.merge(pending_merge))
pending_merge.clear()
merged_segments.append(segment)
case _:
# First loop above may not check all segments when a merge is needed
raise TypeError(type_err)
if pending_merge:
merged_segments.append(TemplateLiteralText.merge(pending_merge))
pending_merge.clear()
self._segments = tuple(merged_segments)
return self
@property
def raw_template(self) -> str:
return self._raw_template
@property
def segments(self) -> tuple[TemplateLiteralText|TemplateLiteralField]:
return self._segments
def __len__(self) -> int:
return len(self._segments)
def __iter__(self) -> Iterable[TemplateLiteralText|TemplateLiteralField]:
return iter(self._segments)
# Note: template literals do NOT define any relative ordering
def __eq__(self, other):
if not isinstance(other, TemplateLiteral):
return NotImplemented
return (
self._raw_template == other._raw_template
and self._segments == other._segments
and self.field_values == other.field_values
and self.format_specifiers == other.format_specifiers
)
def __repr__(self) -> str:
return (f"{type(self).__name__}(r{self._raw!r}, "
f"{', '.join(map(repr, self._segments))})")
def __format__(self, format_specifier) -> str:
# When formatted, render to a string, and then use string formatting
return format(self.render(), format_specifier)
def render(self, *, render_template=''.join, render_text=str, render_field=format):
... # See definition of the template rendering semantics below
def __add__(self, other) -> TemplateLiteral|NotImplemented:
if isinstance(other, TemplateLiteral):
combined_raw_text = self._raw + other._raw
combined_segments = self._segments + other._segments
return TemplateLiteral(combined_raw_text, *combined_segments)
if isinstance(other, str):
# Treat the given string as a new raw text segment
combined_raw_text = self._raw + other
combined_segments = self._segments + (TemplateLiteralText(other),)
return TemplateLiteral(combined_raw_text, *combined_segments)
return NotImplemented
def __radd__(self, other) -> TemplateLiteral|NotImplemented:
if isinstance(other, str):
# Treat the given string as a new raw text segment. This effectively
# has precedence over string concatenation in CPython due to
# https://github.com/python/cpython/issues/55686
combined_raw_text = other + self._raw
combined_segments = (TemplateLiteralText(other),) + self._segments
return TemplateLiteral(combined_raw_text, *combined_segments)
return NotImplemented
def __mul__(self, other) -> TemplateLiteral|NotImplemented:
try:
factor = operator.index(other)
except TypeError:
return NotImplemented
if not self or factor == 1:
return self
if factor < 1:
return TemplateLiteral("")
repeated_text = self._raw_template * factor
repeated_segments = self._segments * factor
return TemplateLiteral(repeated_text, *repeated_segments)
__rmul__ = __mul__
(注意:这是一个说明性的示例实现,types.TemplateLiteral
的确切编译时构造方法和内部数据管理细节被视为实现细节,不受 PEP 规范。但是,上述代码规范了 types.TemplateLiteral
实例上公共 API 的预期构建后行为,以及在运行时构建模板实例的构造函数签名)
模板字面量表达式的结果是此类型的实例,而不是已渲染的字符串。只有在实例的 render
方法被调用时(无论是直接调用还是通过 __format__
间接调用),才会进行渲染。
编译器会将以下详细信息传递给模板字面量以供后续使用
- 包含源代码中编写的原始模板的字符串
- 模板片段的序列,每个片段可以是
- 文字文本片段(一个常规的 Python 字符串,也提供对其原始形式的访问)
- 解析的模板插值字段,指定插值表达式的文本(作为常规字符串)、其计算结果、格式说明符文本(任何替换字段都作为 f-string 提前计算)和转换说明符文本(作为常规字符串)
原始模板只是作为字符串的模板字面量。默认情况下,它用于提供模板字面量的人类可读表示形式,但模板渲染器也可以将其用于其他目的(例如作为缓存查找键)。
解析的模板结构取自 PEP 750,由一系列对应于模板字符串中文本片段和插值字段的模板片段组成。
这种方法旨在允许编译器按顺序完全处理模板的每个片段,然后最终发出代码将所有模板片段传递给模板字面量构造函数。
例如,假设以下运行时值
names = ["Alice", "Bob", "Carol", "Eve"]
field_width = 10
def expressions():
return 42
提案部分的模板将在运行时表示为
TemplateLiteral(
r"Substitute {names:>{field_width}} and {expressions()!r} at runtime",
TemplateLiteralText(r"Substitute "),
TemplateLiteralField("names", ["Alice", "Bob", "Carol", "Eve"], ">10", ""),
TemplateLiteralText(r" and "),
TemplateLiteralField("expressions()", 42, "", "r"),
)
渲染模板
TemplateLiteral.render
的实现根据以下渲染器定义了渲染过程
- 一个整体的
render_template
操作,它定义了如何将渲染后的文本和字段片段序列组合成完全渲染的结果。默认模板渲染器是使用''.join
的字符串连接。 - 一个针对每个文本片段的
render_text
操作,它接收模板中的单个文字文本片段。默认文本渲染器是内置的str
构造函数。 - 一个针对每个字段片段的
render_field
操作,它接收模板中替换字段的值、格式说明符和转换说明符。默认字段渲染器是format()
内置函数。
给定上述解析的模板表示,模板渲染的语义将等效于以下内容
def render(self, *, render_template=''.join, render_text=str, render_field=format):
rendered_segments = []
for segment in self._segments:
match segment:
case TemplateLiteralText() as text_segment:
rendered_segments.append(render_text(text_segment))
case TemplateLiteralField() as field_segment:
rendered_segments.append(render_field(*field_segment[1:]))
return render_template(rendered_segments)
格式说明符
t-string 中字段说明符的语法和处理定义为与 f-string 相同。
这包括允许字段说明符本身包含 f-string 替换字段。字段说明符的原始文本(不处理任何替换字段)作为完整原始模板字符串的一部分保留。
解析的字段说明符接收已解析这些替换的字段说明符字符串。 :
前缀也被省略。
除了在解析期间将格式说明符与替换表达式分离之外,插值模板解析器将格式说明符视为不透明字符串——为其分配语义(或者,或者禁止其使用)由渲染器在渲染时处理。
转换说明符
除了对 a
、r
和 s
转换说明符的现有支持外,str.format()
和 str.format_map()
将更新为接受 ()
作为转换说明符,表示“调用插值值”。
在 PEP 701 将转换说明符限制为 NAME
令牌的情况下,此 PEP 将改为允许 FSTRING_MIDDLE
令牌(因此仅不允许 {
、}
和 :
)。进行此更改的主要目的是支持使用 !()
转换说明符进行延迟字段渲染,但也允许自定义渲染函数在定义自己的转换说明符时拥有更大的灵活性,而不是默认的 format()
字段渲染器定义的那些说明符。
转换说明符仍被视为普通字符串,并且不支持使用替换字段。
解析的转换说明符接收省略了 !
前缀的转换说明符字符串。
为了允许自定义模板渲染器定义自己的自定义转换说明符,而不会导致默认渲染器失败,转换说明符将允许包含以第二个 !
字符为前缀的自定义后缀。也就是说,!!<custom>
、!a!<custom>
、!r!<custom>
、!s!<custom>
和 !()!<custom>
都将是模板字面量中有效的转换说明符。
如上所述,默认渲染支持在 PEP 3101 中定义的原始 !a
、!r
和 !s
转换说明符,以及在此 PEP 中定义的新 !()
延迟字段评估转换说明符。默认渲染会忽略任何自定义转换说明符后缀。
标准转换说明符与渲染字段时在插值值上调用的特殊方法之间的完整映射
- 无转换(空字符串):
__format__
(以格式说明符作为参数) a
:__repr__
(与ascii()
内置函数相同)r
:__repr__
(与repr()
内置函数相同)s
:__str__
(与str
内置函数相同)()
:__call__
(无参数)
发生转换时,__format__
(带格式说明符)将在转换结果上调用,而不是在原始对象上调用。
对 format()
的更改以及 operator.convert_field()
的添加使得自定义渲染器也可以轻松支持标准转换说明符。
f-string 本身将不支持新的 !()
转换说明符(因为它在值插值和值渲染始终同时发生时是冗余的)。它们也不支持使用自定义转换说明符(因为渲染函数在编译时已知,并且不使用自定义说明符)。
operator 模块中的新字段转换 API
为了支持在自定义模板渲染函数中应用标准转换说明符,将添加一个新的 operator.convert_field()
函数
def convert_field(value, conversion_spec=''):
"""Apply the given string formatting conversion specifier to the given value"""
std_spec, sep, custom_spec = conversion_spec.partition("!")
match std_spec:
case '':
return value
case 'a':
return ascii(value)
case 'r':
return repr(value)
case 's':
return str(value)
case '()':
return value()
if not sep:
err = f"Invalid conversion specifier {std_spec!r}"
else:
err = f"Invalid conversion specifier {std_spec!r} in {conversion_spec!r}"
raise ValueError(f"{err}: expected '', 'a', 'r', 's' or '()')
向 format 添加转换说明符参数
format()
内置函数的签名和行为将更新
def format(value, format_spec='', conversion_spec=''):
if conversion_spec:
value_to_format = operator.convert_field(value)
else:
value_to_format = value
return type(value_to_format).__format__(value, format_spec)
如果给定非空的转换说明符,则在查找 __format__
方法之前,将使用 operator.convert_field()
转换该值。
__format__
特殊方法的签名没有改变(只有格式说明符由被格式化的对象处理)。
结构化类型和鸭子类型
为了允许自定义渲染器接受替代的插值模板实现(而不是与本机模板字面量类型紧密耦合),以下结构协议将被添加到 typing
模块中
@runtime_checkable
class TemplateText(Protocol):
# Renamed version of PEP 750's Decoded protocol
def __str__(self) -> str:
...
raw: str
@runtime_checkable
class TemplateField(Protocol):
# Renamed and modified version of PEP 750's Interpolation protocol
def __len__(self):
...
def __getitem__(self, index: int):
...
def __str__(self) -> str:
...
expr: str
value: Any
format_spec: str | None = None
conversion_spec: str | None = None
@runtime_checkable
class InterpolationTemplate(Protocol):
# Corresponds to PEP 750's Template protocol
def __iter__(self) -> Iterable[TemplateText|TemplateField]:
...
raw_template: str
请注意,结构协议 API 比为 TemplateLiteralText
、TemplateLiteralField
和 TemplateLiteral
定义的完整实现 API 窄得多。
希望接受插值模板并为其定义特定处理方式的代码,而不引入对typing
模块的依赖,或将代码限制为处理具体的模板字面量类型,应该改为对raw_template
进行属性是否存在检查。
编写自定义渲染器
编写自定义渲染器不需要任何特殊语法。相反,自定义渲染器是处理插值模板的普通可调用对象,可以通过使用备选的render_template
、render_text
和/或render_field
实现调用render()
方法,或直接访问模板的数据属性来实现。
例如,以下函数将使用对象的repr
实现而不是其原生格式化支持来渲染模板
def repr_format(template):
def render_field(value, format_spec, conversion_spec):
converted_value = operator.convert_field(value, conversion_spec)
return format(repr(converted_value), format_spec)
return template.render(render_field=render_field)
所示的客户渲染器尊重原始模板中的转换说明符,但也可以忽略它们并直接渲染插值值
def input_repr_format(template):
def render_field(value, format_spec, __):
return format(repr(value), format_spec)
return template.render(render_field=render_field)
在编写自定义渲染器时,请注意,整体渲染操作的返回类型由传入的render_template
可调用的返回类型决定。虽然对于格式相关的用例,这仍然是一个字符串,但允许生成非字符串对象。例如,自定义SQL模板渲染器可能涉及sqlalchemy.sql.text
调用,该调用生成一个SQL Alchemy查询对象。与子进程调用相关的模板渲染器可以生成适合传递给subprocess.run
的字符串序列,或者它甚至可以直接调用subprocess.run
,并返回结果。
只要与期望该行为的render_template
实现配对,render_text
和render_field
也可以返回非字符串。
还支持使用PEP 750中描述的模式匹配风格的自定义渲染器
# Use the structural typing protocols rather than the concrete implementation types
from typing import InterpolationTemplate, TemplateText, TemplateField
def greet(template: InterpolationTemplate) -> str:
"""Render an interpolation template using structural pattern matching."""
result = []
for segment in template:
match segment:
match segment:
case TemplateText() as text_segment:
result.append(text_segment)
case TemplateField() as field_segment:
result.append(str(field_segment).upper())
return f"{''.join(result)}!"
表达式求值
与f-字符串一样,从插值模板中提取的子表达式在模板字面量出现的上下文中进行评估。这意味着表达式可以完全访问局部、非局部和全局变量。可以在{}
内部使用任何有效的Python表达式,包括函数和方法调用。
由于替换表达式是在字符串出现在源代码中的位置进行评估的,因此与表达式的内容本身相关的安全问题并不多,因为您也可以编写相同的表达式并使用运行时字段解析
>>> bar=10
>>> def foo(data):
... return data + 20
...
>>> str(t'input={bar}, output={foo(bar)}')
'input=10, output=30'
本质上等价于
>>> 'input={}, output={}'.format(bar, foo(bar))
'input=10, output=30'
处理代码注入攻击
PEP 498格式化的字符串语法使得编写如下代码具有潜在的吸引力
runquery(f"SELECT {column} FROM {table};")
runcommand(f"cat {filename}")
return_response(f"<html><body>{response.body}</body></html>")
如果任何正在插值的变量恰好来自不受信任的来源,则所有这些都代表着代码注入攻击的潜在媒介。本PEP中的具体建议旨在简化编写特定于用例的渲染器,这些渲染器可以针对相关的安全上下文适当地处理插值值的引用。
runquery(sql(t"SELECT {column} FROM {table} WHERE column={value};"))
runcommand(sh(t"cat {filename}"))
return_response(html(t"<html><body>{response.body}</body></html>"))
本PEP不涵盖立即将所有此类渲染器添加到标准库中(尽管建议使用一个用于shell转义的渲染器),而是建议确保第三方库可以轻松地提供这些渲染器,并可能在以后的日期合并到标准库中。
随着时间的推移,预计处理潜在危险字符串输入的API可能会更新为原生接受插值模板,从而允许通过简单地将f
字符串前缀替换为t
来修复有问题的代码示例。
runquery(t"SELECT {column} FROM {table};")
runcommand(t"cat {filename}")
return_response(t"<html><body>{response.body}</body></html>")
建议在shlex
模块中包含一个渲染器,旨在为访问外部程序提供更类似于POSIX shell的体验,而不会带来运行os.system
或在使用subprocess
模块API时启用系统shell带来的重大风险。此渲染器将提供一个受Julia编程语言提供的接口启发的运行外部程序的接口,只是将基于反引号的\`cat $filename\`
语法替换为t"cat {filename}"
样式的模板字面量。请参阅添加到shlex的shell转义渲染器部分中的更多信息。
错误处理
在处理插值表达式时,可能会发生编译时错误或运行时错误。编译时错误仅限于在将模板字符串解析为其组件元组时可以检测到的错误。这些错误都会引发SyntaxError。
不匹配的花括号
>>> t'x={x'
File "<stdin>", line 1
t'x={x'
^
SyntaxError: missing '}' in template literal expression
无效的表达式
>>> t'x={!x}'
File "<fstring>", line 1
!x
^
SyntaxError: invalid syntax
运行时错误发生在评估模板字符串内的表达式以创建模板字面量对象之前。请参阅PEP 498以获取一些示例。
不同的渲染器也可能对可接受的插值表达式和其他格式细节施加额外的运行时约束,这些约束将作为运行时异常报告。
添加到shlex
的shell转义渲染器
作为参考实现,可以将用于安全POSIX shell转义的渲染器添加到shlex
模块中。此渲染器将被称为sh
,并且等效于在模板字面量中的每个字段值上调用shlex.quote
。
因此
os.system(shlex.sh(t'cat {myfile}'))
将具有与以下相同的行为
os.system('cat ' + shlex.quote(myfile)))
实现将是
def sh(template: TemplateLiteral):
def render_field(value, format_spec, conversion_spec)
field_text = format(value, format_spec, conversion_spec)
return quote(field_text)
return template.render(render_field=render_field)
添加shlex.sh
不会更改subprocess
文档中现有的告诫,即最好避免传递shell=True
,也不会更改来自os.system()
文档到更高级别的subprocess
API的引用。
对 subprocess 模块的更改
通过在shlex模块中添加额外的渲染器,以及添加模板字面量,subprocess
模块可以更改为将模板字面量作为Popen
的附加输入类型接受,因为它已经接受序列或字符串,并且每个都有不同的行为。
通过添加模板字面量,subprocess.Popen
(以及作为回报,其所有更高级别的函数,如subprocess.run()
)可以以安全的方式(至少在POSIX系统上)接受字符串。
例如
subprocess.run(t'cat {myfile}', shell=True)
将自动使用本PEP中提供的shlex.sh
渲染器。因此,在类似于以下的subprocess.run
调用中使用shlex
subprocess.run(shlex.sh(t'cat {myfile}'), shell=True)
将是多余的,因为run
将自动通过shlex.sh
渲染任何模板字面量
或者,当subprocess.Popen
在没有shell=True
的情况下运行时,它仍然可以为子进程提供更符合人体工程学的语法。例如
subprocess.run(t'cat {myfile} --flag {value}')
将等效于
subprocess.run(['cat', myfile, '--flag', value])
或者,更准确地说
subprocess.run(shlex.split(f'cat {shlex.quote(myfile)} --flag {shlex.quote(value)}'))
它将首先使用shlex.sh
渲染器(如上所述),然后对结果使用shlex.split
。
在subprocess.Popen._execute_child
内部的实现将如下所示
if hasattr(args, "raw_template"):
import shlex
if shell:
args = [shlex.sh(args)]
else:
args = shlex.split(shlex.sh(args))
如何教授
本PEP有意包含两个始终在教学环境中可用的标准渲染器:format()
内置函数和新的shlex.sh
POSIX shell渲染器。
这两个渲染器可以一起用于在学生最初接触到f-字符串的字符串格式化之后,构建对延迟渲染的初步理解。这种初步理解的目标是允许学生有效地使用模板字面量,并结合预先存在的模板渲染函数。
例如,可以介绍f"{'some text'}"
、f"{value}"
、f"{value!r}"
、f"{callable()}"
。
然后可以将相同的操作重写为format(t"{'some text'}")
、format(t"{value}")
、format(t"{value!r}")
、format(t"{callable()}")
,以说明急切渲染形式和延迟渲染形式之间的关系。
然后可以通过将模板字面量存储为局部变量,并分别查看其表示形式与format
调用的结果,来进一步研究“模板定义时间”(或“插值时间”)和“模板渲染时间”之间的差异。此时,可以引入t"{callable!()}"
语法来区分在模板定义时调用的字段表达式和在模板渲染时调用的字段表达式。
最后,可以探索f"{'some text'}"
、format(t"{'some text'}")
和shlex.sh(t"{'some text'}")
结果之间的差异,以说明默认渲染函数和自定义渲染函数之间存在潜在差异。
然后,实际定义您自己的自定义模板渲染函数将是一个单独的更高级的主题(类似于学生在学习如何编写自己的自定义装饰器和上下文管理器之前,通常被教导如何使用它们的方式)。
PEP 750 延迟渲染主题的字符串标记 包含了更多关于延迟渲染主题教学方面的想法。
讨论
请参考 PEP 498 以获取之前的讨论,因为其中的一些要点也适用于此 PEP。 PEP 750 的设计讨论也高度相关,因为该 PEP 激发了当前设计的几个方面。
对二进制插值的支持
由于 f-字符串不处理字节字符串,因此 t-字符串也不会处理。
与仅 str 接口的互操作性
为了与仅接受字符串的接口进行互操作,插值模板仍然可以使用 format()
进行预渲染,而不是将渲染委托给被调用的函数。
这反映了与 PEP 498 的关键区别,后者 *始终* 积极应用默认渲染,没有任何方法可以将渲染器的选择委托给代码的另一个部分。
保留原始模板字符串
此 PEP 的早期版本未能使原始模板字符串在模板字面量上可用。保留它可以提供更具吸引力的模板表示,并能够精确地重建原始字符串,包括表达式文本和格式说明符中任何急切渲染的替换字段的详细信息。
创建丰富对象而不是全局名称查找
此 PEP 的早期版本使用了 __interpolate__
内置函数,而不是为插值函数以后使用创建一种新类型的对象。创建一个具有有用默认渲染器的丰富描述性对象,使得更容易支持插值语义的自定义。
建立在 f-字符串之上而不是替换它们
此 PEP 的早期版本试图完全替代 PEP 498 (f-字符串)。随着该 PEP 以及最近的 PEP 701 的接受,此 PEP 可以构建一个更灵活的延迟渲染功能,建立在现有的 f-字符串急切渲染之上。
假设存在 f-字符串作为支持功能,简化了此 PEP 中的一些提案方面(例如如何处理格式说明符中的替换字段)。
定义重复和连接语义
此 PEP 明确定义了 TemplateLiteral
和 TemplateLiteralText
的重复和连接语义。虽然不是严格必要的,但定义这些预计将使类型更容易在历史上仅支持常规字符串的代码中使用。
用于延迟字段评估的新转换说明符
PEP 750 的初始发布版本默认为所有插值字段的延迟求值。虽然它随后更新为默认为急切求值(就像 f-字符串和此 PEP 中发生的那样),但围绕该主题的讨论引发了提供一种方法来指示渲染函数插值字段值应该在渲染时被调用,而不是在未经修改的情况下被使用的想法。
由于 PEP 750 也将转换说明符的处理延迟到评估时间,因此有人提出,在没有参数的情况下调用 __call__
可以被视为类似于调用 __repr__
(!a
, !r
) 或 __str__
(!s
) 的现有转换说明符。
因此,此 PEP 已更新,也将转换说明符处理作为渲染函数的责任,并引入 !()
作为延迟求值的新转换说明符。
添加 operator.convert_field()
并更新 format()
内置函数,然后是为想要接受默认转换说明符的渲染函数实现提供适当的支持的问题。
允许自定义渲染器使用任意转换说明符
接受 !()
作为新的转换说明符,必然需要更新解析器接受的转换说明符语法(它们目前仅限于标识符)。然后提出了一个问题,即 t-字符串编译是否应该强制执行 f-字符串编译强加的额外限制:转换说明符必须恰好是 !a
、!r
或 !s
之一。
由于 t-字符串已更新为在编译时允许 !()
,因此将转换说明符视为与渲染函数相关,类似于格式说明符与单个对象的格式化方式:除了出于解析原因而排除的一些字符外,它们都是自由文本字段,其含义由使用函数或对象决定。这减少了在模板的格式说明符中引入渲染器特定元格式的诱惑(因为任何渲染器特定信息都可以放在转换说明符中)。
仅保留一个新的字符串前缀
此 PEP 与 PEP 750 的主要区别在于,后者旨在启用任意字符串前缀的使用,而不是要求创建然后传递给其他 API 的模板字面量实例。例如,PEP 750 将允许此 PEP 中描述的 sh
渲染用作 sh"cat {somefile}"
,而不是要求显式创建模板字面量,然后传递给常规函数调用(如 sh(t"cat {somefile}")
)。
PEP 作者更喜欢第二种拼写的主要原因是,它使读者更容易理解正在发生的事情:正在创建一个模板字面量实例,然后将其传递给知道如何对插值模板实例执行某些有用操作的可调用对象。
来自 PEP 750 作者之一的 草案提案 还建议,静态类型检查器将能够像从使用显式函数调用的表单中那样容易地推断特定领域特定语言的使用,就像他们能够从直接标记的字符串中推断出来一样。
由于标记字符串语法至少可以说降低了人类读者的清晰度,而没有提高构造的整体表达能力,因此从最小的可行提案(单个新的字符串前缀)开始,然后在将来重新审视将泛化到任意前缀的潜在价值似乎是合理的。
作为一个较小但仍然真实的考虑因素,仅对这种用例使用单个新的字符串前缀,为将来定义替代前缀留下了可能性,这些前缀仍然生成 TemplateLiteral
对象,但在字符串中使用不同的语法来定义插值字段(参见下面的 i18n 讨论)。
推迟考虑更简洁的延迟评估语法
在延迟求值的讨论过程中,{-> expr}
被 建议 作为已经支持的基于 lambda
的语法的潜在语法糖:{(lambda: expr)}
(在现有语法中需要括号以避免将 :
字符误解为指示格式说明符的开始)。
虽然添加这种拼写将补充此 PEP 中提出的渲染时间函数调用语法(即,编写 {-> expr!()}
以在渲染时评估任意表达式),但 PEP 作者认为,如果此 PEP 或 PEP 750 被接受,最好将其留给未来的 PEP 来处理。
推迟考虑可能的日志记录集成
日志记录模块面临的挑战之一是,我们之前无法设计出一种合理的迁移策略来避免使用 printf 样式的格式化。虽然日志记录模块允许格式化程序指定使用 str.format()
或 string.Template
样式替换,但确保以这种方式编写的消息仅由期望该语法的日志记录格式化程序处理可能会很麻烦。
日志消息的运行时解析和插值开销也对出于监控目的对运行时事件进行广泛日志记录提出了问题。
虽然超出了此初始 PEP 的范围,但模板字面量支持可能会添加到日志记录模块的事件报告 API 中,允许使用以下形式捕获相关详细信息
logging.debug(t"Event: {event}; Details: {data}")
logging.critical(t"Error: {error}; Details: {data}")
而不是历史的 mod 格式化样式
logging.debug("Event: %s; Details: %s", event, data)
logging.critical("Error: %s; Details: %s", event, data)
由于模板字面量作为普通参数传入,因此其他关键字参数也将可用
logging.critical(t"Error: {error}; Details: {data}", exc_info=True)
此 PEP 中描述的标准化延迟字段求值的方法主要基于此假设集成到日志记录模块中的预期需求
logging.debug(t"Eager evaluation of {expensive_call()}")
logging.debug(t"Lazy evaluation of {expensive_call!()}")
logging.debug(t"Eager evaluation of {expensive_call_with_args(x, y, z)}")
logging.debug(t"Lazy evaluation of {(lambda: expensive_call_with_args(x, y, z))!()}")
日志记录格式化程序的定义是否会更新以支持模板字符串是一个悬而未决的问题,但如果更新了,定义应该在日志记录记录上 查找 而不是急切地解释的字段的最可能方法是简单地对它们进行转义,以便它们作为字面量文本的一部分可用
proc_id = get_process_id()
formatter = logging.Formatter(t"{{asctime}}:{proc_id}:{{name}}:{{levelname}}{{message}}")
推迟考虑在 i18n 用例中的可能用途
此 PEP 的最初激励用例是为 i18n(国际化)翻译提供更简洁的语法,因为这需要访问原始的未修改模板。因此,它专注于与 Python 的 string.Template
格式化和 Mozilla 的 l20n 项目中使用的替换语法兼容。
然而,随后的讨论表明,在国际化 (i18n) 使用案例中,需要考虑一些重要的额外因素,这些因素不会影响处理插值到安全敏感上下文(如 HTML、系统 shell 和数据库查询)或以开发团队的首选语言(而不是最终用户的母语)生成应用程序调试消息等更简单的案例。
由于认识到这一点,PEP 决定使用最初在str.format()
中定义,并在PEP 3101中首次定义,随后作为PEP 498的基础的替换语法。
虽然理论上可以更新string.Template
以支持从原生模板字面量创建实例,并实现结构化typing.Template
协议,但 PEP 作者尚未发现这样做有任何实际益处。
但是,此 PEP 中使用的“仅一个字符串前缀”方法的一个重要好处是,虽然它将现有的 f-字符串插值语法推广到通过 t-字符串支持延迟渲染,但它并不意味着这应该是 Python 应该提供的唯一受编译器支持的插值语法。
最值得注意的是,它为另一种“t$-字符串”语法敞开了大门,该语法将允许使用基于PEP 292的插值语法(而不是基于PEP 3101的语法)创建TemplateLiteral
实例。
template = t$”Substitute $words and ${other_values} at runtime”
这样创建的模板与从常规 t-字符串创建的模板之间唯一的运行时区别在于其raw_template
属性的内容。
推迟非 POSIX shell 的转义渲染支持
shlex.quote()
的工作原理是将正则表达式字符集[\w@%+=:,./-]
分类为安全,并将所有其他字符视为不安全,因此需要对包含它们的字符串进行引用。然后,使用的引用机制特定于 POSIX shell 中字符串引用的工作方式,因此在运行不遵循 POSIX shell 字符串引用规则的 shell 时,无法信任它。
例如,在使用遵循 POSIX 引用规则的 shell 时,运行subprocess.run(f'echo {shlex.quote(sys.argv[1])}', shell=True)
是安全的。
$ cat > run_quoted.py
import sys, shlex, subprocess
subprocess.run(f"echo {shlex.quote(sys.argv[1])}", shell=True)
$ python3 run_quoted.py pwd
pwd
$ python3 run_quoted.py '; pwd'
; pwd
$ python3 run_quoted.py "'pwd'"
'pwd'
但在从 Python 调用cmd.exe
(或 Powershell)运行 shell 时,仍然不安全。
S:\> echo import sys, shlex, subprocess > run_quoted.py
S:\> echo subprocess.run(f"echo {shlex.quote(sys.argv[1])}", shell=True) >> run_quoted.py
S:\> type run_quoted.py
import sys, shlex, subprocess
subprocess.run(f"echo {shlex.quote(sys.argv[1])}", shell=True)
S:\> python3 run_quoted.py "echo OK"
'echo OK'
S:\> python3 run_quoted.py "'& echo Oh no!"
''"'"'
Oh no!'
解决此标准库限制超出了本 PEP 的范围。
致谢
参考文献
- %-格式化
- str.format
- string.Template 文档
- PEP 215:字符串插值
- PEP 292:更简单的字符串替换
- PEP 3101:高级字符串格式化
- PEP 498:文字字符串格式化
- PEP 675:任意文字字符串类型
- PEP 701:f-字符串的语法形式化
- FormattableString 和 C# 原生字符串插值
- C# 中的 IFormattable 接口(请参阅备注以获取全球化说明)
- Javascript 中的 TemplateLiterals
- 在 Julia 中运行外部命令
版权
本文档置于公共领域或根据 CC0-1.0-Universal 许可证,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0501.rst
上次修改时间:2024-09-13 08:53:53 GMT