PEP 502 – 字符串插值 - 扩展讨论
- 作者:
- Mike G. Miller
- 状态:
- 已拒绝
- 类型:
- 信息
- 创建:
- 2015 年 8 月 10 日
- Python 版本:
- 3.6
摘要
PEP 498: 字面量字符串插值,它提出了“格式化字符串”,于 2015 年 9 月 9 日被接受。在其设计阶段提供的其他背景和基本原理在下面详细说明。
为了回顾该 PEP,引入了一个字符串前缀,将字符串标记为要渲染的模板。这些格式化字符串可以包含一个或多个基于 现有语法 的表达式 str.format()
。 [10] [11] 格式化字符串在编译时扩展为传统的字符串格式化操作,其文本中的给定表达式被提取并作为位置参数传递。
在运行时,对得到的表达式求值以根据给定规范渲染字符串。
>>> location = 'World'
>>> f'Hello, {location} !' # new prefix: f''
'Hello, World !' # interpolated result
可以认为格式字符串只是简化对 str.format()
的传统调用的语法糖。
PEP 状态
此 PEP 被拒绝,因为它使用了基于意见的语气而不是事实语气。此 PEP 也被认为没有必要,因为 PEP 498 已经编写完毕,应该作为容纳设计决策细节的地方。
动机
尽管 Python 中的字符串格式化和操作功能很丰富,但在缺乏方便的字符串插值语法方面却有所不足。与其他具有类似用例的动态脚本语言相比,构建类似字符串所需的代码量要高得多,而有时由于冗长、密集语法或标识符重复导致可读性降低。
这些困难在原始的 python-ideas 帖子 中有详细描述,该帖子开始了滚雪球(它变成了 PEP 498)的滚动。 [1]
此外,用 Python 3 的更一致的 print 函数替换 print 语句 (PEP 3105) 增加了额外的少量负担,即一组额外的括号需要输入和阅读。结合当前字符串格式化解决方案的冗长,这使得本来就简单的语言在与同类语言相比处于不利地位。
echo "Hello, user: $user, id: $id, on host: $hostname" # bash
say "Hello, user: $user, id: $id, on host: $hostname"; # perl
puts "Hello, user: #{user}, id: #{id}, on host: #{hostname}\n" # ruby
# 80 ch -->|
# Python 3, str.format with named parameters
print('Hello, user: {user}, id: {id}, on host: {hostname}'.format(**locals()))
# Python 3, worst case
print('Hello, user: {user}, id: {id}, on host: {hostname}'.format(user=user,
id=id,
hostname=
hostname))
在 Python 中,用标准宽度的一行代码格式化和打印包含多个变量的字符串明显更难,并且更冗长,缩进会加剧这个问题。
对于小型项目、系统编程、shell 脚本替换甚至单行代码等用例,其中消息格式化复杂度尚未封装,这种冗长很可能导致许多开发人员和管理员多年来选择其他语言。
基本原理
目标
格式化字符串的设计目标如下
局限性
与从 Unix 及其 shell 获取设计线索的其他语言相反,与 Javascript 相同,Python 指定了单引号 ('
) 和双引号 ("
) ASCII 引号字符来包含字符串。现在选择其中一个来启用插值,而将另一个留给非插值字符串是不合理的。其他字符,例如“反引号”(或重音符 `
) 也 受到历史的限制,作为 repr()
的快捷方式。
这给我们留下了一些设计此类功能的选项
- 一个运算符,如通过
%
进行的 printf 风格字符串格式化。 - 一个类,如
string.Template()
。 - 一个方法或函数,如
str.format()
。 - 新语法,或
- 新的字符串前缀标记,如众所周知的
r''
或u''
。
上面前三个选项已经成熟。每一个都有特定的用例和缺点,但也存在前面提到的冗长和视觉噪音问题。所有选项将在下一节中讨论。
背景
格式化字符串基于一些现有的技术和提案以及我们从中学到的东西。为了保持可读性和错误预防的设计目标,以下示例使用命名参数而不是位置参数。
假设我们有以下字典,并且希望将它的项打印为用户友好的字符串。
>>> params = {'user': 'nobody', 'id': 9, 'hostname': 'darkstar'}
通过运算符的 printf 风格格式化
这种 古老的技术 仍然有其用途,例如与基于字节的协议、简单情况下的简单性以及许多程序员的熟悉程度。
>>> 'Hello, user: %(user)s, id: %(id)s, on host: %(hostname)s' % params
'Hello, user: nobody, id: 9, on host: darkstar'
在这种形式下,考虑到先决条件字典创建,这种技术很冗长,有点嘈杂,但相对易读。其他问题是运算符除了原始字符串之外只能接受一个参数,这意味着多个参数必须在元组或字典中传递。此外,在传递的参数数量、预期类型、缺少键或忘记尾部类型(例如 s
或 d
)方面很容易出错。
string.Template 类
来自 PEP 292(更简单的字符串替换)的 string.Template
类 是一种有意简化的设计,使用熟悉的 shell 插值语法,具有 安全替换功能,它在 shell 和国际化工具中找到了主要的用例。
Template('Hello, user: $user, id: ${id}, on host: $hostname').substitute(params)
虽然也很冗长,但字符串本身是易读的。虽然功能有限,但它很好地满足了它的要求。它对于许多情况来说不够强大,这有助于防止经验不足的用户出现问题,以及避免与来自第三方的中等信任的输入 (i18n) 相关的问题。不幸的是,它需要足够的代码来阻止它用于即席字符串插值,除非封装在 便利库 中,例如 flufl.i18n
。
PEP 215 - 字符串插值
PEP 215 是一个以前的提议,与这个提议有许多共同点。显然,当时世界还没有准备好接受它,但考虑到最近在许多其他语言中的支持,它的时代可能已经到来。
它包含的大量美元符号 ($
) 字符可能让它看起来像 Python 的死对头 Perl,并且可能导致了这个 PEP 没有被接受。它被以下提议取代。
str.format() 方法
PEP 3101 的 str.format()
语法 是现有选项中最新的和最现代的。它也比其他选项功能更强大,通常更容易阅读。它避免了以前技术中的许多缺点和限制。
但是,由于其必要的函数调用和参数传递,它在字符串字面量中的各种情况下从冗长到非常冗长。
>>> 'Hello, user: {user}, id: {id}, on host: {hostname}'.format(**params)
'Hello, user: nobody, id: 9, on host: darkstar'
# when using keyword args, var name shortening sometimes needed to fit :/
>>> 'Hello, user: {user}, id: {id}, on host: {host}'.format(user=user,
id=id,
host=hostname)
'Hello, user: nobody, id: 9, on host: darkstar'
这里说明了基于方法的方法的冗长。
PEP 498 – 字面量字符串格式化
PEP 498 定义并讨论了格式化字符串,如上面的 摘要 中所述。
它还,对于那些第一次接触它的人来说有点争议,引入了将格式化字符串扩展为支持任意表达式的想法。这将在 被拒绝的想法 下的限制语法部分进一步讨论。
PEP 501 – 翻译就绪的字符串插值
补充的 PEP 501 将国际化作为一等关注点引入讨论,其提议使用 i 前缀、与 ES6 (Javascript) 兼容的 string.Template
语法集成、延迟渲染以及对象返回值。
其他语言中的实现
字符串插值现在在各个行业使用的各种编程语言中得到很好的支持,并且正在逐渐成为一种标准。它以 str.format()
风格的语法为中心,有一些细微的变体,并添加了任意表达式以扩展实用性。
在 动机 部分,我们展示了 Bash、Perl 和 Ruby 中存在多么方便的插值语法。让我们看看它们对表达式的支持。
Bash
Bash 支持字符串内的一些任意结构,甚至递归结构。
> echo "user: $USER, id: $((id + 6)) on host: $(echo is $(hostname))"
user: nobody, id: 15 on host: is darkstar
Perl
Perl 也具有任意表达式结构,可能不太为人所知
say "I have @{[$id + 6]} guanacos."; # lists
say "I have ${\($id + 6)} guanacos."; # scalars
say "Hello { @names.join(', ') } how are you?"; # Perl 6 version
Ruby
Ruby 允许在其插值字符串中使用任意表达式
puts "One plus one is two: #{1 + 1}\n"
其他
让我们来看看最近实现字符串插值的几个不太相似的现代语言。
Scala
Scala 插值 是通过字符串前缀进行的。每个前缀都有不同的结果
s"Hello, $name ${1 + 1}" # arbitrary
f"$name%s is $height%2.2f meters tall" # printf-style
raw"a\nb" # raw, like r''
这些前缀也可以通过扩展 Scala 的 StringContext
类来实现。
- 在双引号内进行显式插值,并使用字面量前缀。
- 支持用户实现的前缀。
- 支持任意表达式。
ES6 (Javascript)
模板字符串 的设计者面临着与 Python 相同的问题,即单引号和双引号都被占用。然而,与 Python 不同的是,“反引号”没有被占用。尽管 存在一些问题,它们还是被选作 ECMAScript 2015 (ES6) 标准的一部分
console.log(`Fifteen is ${a + b} and\nnot ${2 * a + b}.`);
通过实现与标签同名的函数,也支持自定义前缀
function tag(strings, ...values) {
console.log(strings.raw[0]); // raw string is also available
return "Bazinga!";
}
tag`Hello ${ a + b } world ${ a * b}`;
- 在反引号内进行显式插值。
- 支持用户实现的前缀。
- 支持任意表达式。
C#,版本 6
C# 也有一个有用的新 插值功能,并且可以通过 IFormattable
接口 自定义插值
$"{person.Name, 20} is {person.Age:D3} year{(p.Age == 1 ? "" : "s")} old.";
- 在双引号内进行显式插值,并使用
$
前缀。 - 可以使用自定义插值。
- 支持任意表达式。
Apple 的 Swift
在所有字符串上都支持对 Swift 中的任意插值
let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
// message is "3 times 2.5 is 7.5"
- 在双引号内进行隐式插值。
- 支持任意表达式。
- 不能包含 CR/LF。
其他示例
可以在 维基百科 上找到许多其他字符串插值的示例。
现在,已经介绍了背景和历史,让我们继续寻找解决方案。
新语法
这应该是一个最后的手段,因为每个新的语法特性在它所处的脑容量方面都存在成本。然而,在我们列出的可能性中还剩下一个备选方案,如下所示。
新字符串前缀
鉴于 Python 中字符串格式化的历史和向后兼容性,其他语言中的实现,以及除非必要不使用新的语法,因此通过消除而不是独特的见解得出了一个可接受的设计。因此,选择使用字符串前缀来标记插值字符串字面量。
我们还选择了一个表达式语法,该语法重新使用并建立在现有选择中最强大的选择基础上,即 str.format()
,以避免进一步重复功能
>>> location = 'World'
>>> f'Hello, {location} !' # new prefix: f''
'Hello, World !' # interpolated result
PEP 498 – 字面量字符串格式化,深入探讨了此设计的机制和实现。
其他主题
安全
在本节中,我们将描述支持格式字符串的安全情况和采取的预防措施。
- 仅考虑字符串字面量作为格式字符串,而不是将变量作为输入或传递,这使得外部攻击难以实现。
str.format()
及其替代方案 已经处理 了这种情况。 - 在转换期间,既不需要也不使用
locals()
或globals()
,避免了信息泄露。 - 为了消除复杂性以及由于递归深度引起的
RuntimeError
(s),不支持递归插值。
但是,字符串字面量内的错误或恶意代码可能会被忽略。尽管这可以用于一般的代码,但这些表达式位于字符串内意味着它们更容易被隐藏。
通过工具缓解
这样做的理念是,诸如 pyflakes、pylint 或 Pycharm 之类的工具或 linter 可以在包含表达式的字符串内进行检查,并相应地标记它们。由于这在当今的编程语言中是一项常见的任务,因此多语言工具无需专门为 Python 实现此功能,从而大大缩短了实现时间。
在更遥远的将来,还可以检查字符串,以查找超出项目安全策略的结构。
风格指南/注意事项
由于任意表达式可以完成 Python 表达式能够完成的任何操作,因此强烈建议避免在格式字符串内使用可能导致副作用的结构。
在使用模式和真正问题变得明朗之后,可能会编写进一步的指南。
参考实现
PyPI 上的 say 模块 以可调用的接口的形式实现了这里描述的字符串插值,只需付出很小的代价
> pip install say
from say import say
nums = list(range(4))
say("Nums has {len(nums)} items: {nums}")
Ruby 插值的 Python 实现 也已可用。它使用 codecs 模块来完成其工作
> pip install interpy
# coding: interpy
location = 'World'
print("Hello #{location}.")
向后兼容性
通过使用现有的语法并避免当前或历史特性,格式字符串的设计不会干扰现有的代码,并且预计不会造成任何问题。
延迟的想法
国际化
尽管强烈希望集成国际化支持(请参阅 PEP 501),但几乎在每一点上,更精细的细节都存在分歧,使得通用解决方案不太可能: [15]
- 用例不同
- 编译与运行时任务
- 插值语法需要
- 目标受众
- 安全策略
被拒绝的想法
将语法限制为 str.format()
反对支持任意表达式的常见 论据 是
- YAGNI,“你不会用到的。”
- 此功能与 Python 历史上保守的风格不一致。
- 推迟 - 如果需要,可以在将来的版本中实现。
但是,仅支持 str.format()
语法被认为不足以解决问题。例如,在打印之前,通常希望得到对象的简单长度或增量。
从 其他语言中的实现 部分可以看出,整个开发人员社区倾向于同意。字符串插值,包括任意表达式,正在成为现代语言中的行业标准,因为它非常实用。
其他/自定义字符串前缀
正如 其他语言中的实现 部分所见,许多现代语言具有可扩展的字符串前缀,并具有通用接口。这可能是通用化并减少常见情况下的代码行的一种方法。例如,ES6 (Javascript)、Scala、Nim 和 C#(在较小程度上)中都有。但这遭到了 BDFL 的拒绝。 [14]
输入变量的自动转义
虽然在某些情况下很有用,但这被认为会导致对何时何地可以安全地使用字符串表达式存在太多不确定性。这个概念也很难向其他人解释。 [12]
始终将格式字符串变量视为未转义,除非开发人员已显式地转义它们。
环境访问和命令替换
对于系统编程和 shell 脚本替换,直接在表达式字符串中处理环境变量和捕获命令输出将非常有用。但这被拒绝了,因为认为这并不重要,而且看起来太像 bash/perl,这可能会助长不良习惯。 [13]
致谢
- Eric V. Smith 为 PEP 498 的创作和实现做出了贡献。
- 感谢 python-ideas 邮件列表中的每个人,他们拒绝了各种疯狂的想法,帮助保持最终设计重点突出。
参考文献
版权
本文件已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0502.rst
最后修改时间:2023-09-09 17:39:29 GMT