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 设计语言不同,Python(与 Javascript 类似)指定了单引号(')和双引号(")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 也具有任意表达式的构造,也许不如 Bash 广为人知。
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)
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"
- 在双引号内进行隐式插值。
- 支持任意表达式。
- 不能包含回车/换行符。
其他示例
您可以在 Wikipedia 上找到更多字符串插值的示例。
现在已经涵盖了背景和历史,让我们继续寻找解决方案。
新语法
这应该是最后的选择,因为每个新的语法功能都会在它所占据的脑海中付出一定的代价。然而,我们的可能性列表中还剩一个选择,如下所示。
新的字符串前缀
考虑到 Python 中字符串格式化的历史、向后兼容性、其他语言的实现、避免不必要的新语法,可以通过消除法而不是独特的洞察力来达到一个可接受的设计。因此,选择通过字符串前缀标记插值字符串字面值。
我们还选择一种表达式语法,该语法重用并构建在最强的现有选择 str.format() 之上,以避免功能重复。
>>> location = 'World'
>>> f'Hello, {location} !' # new prefix: f''
'Hello, World !' # interpolated result
PEP 498 – 字面字符串格式化,详细介绍了此设计的工作原理和实现。
附加主题
安全性
本节将描述有关格式字符串的安全情况和为支持格式字符串而采取的预防措施。
- 仅考虑了字符串字面值作为格式字符串,而不是作为输入或传递的变量,这使得外部攻击难以实现。
str.format()和其他替代方法已经处理了这个用例。 - 在转换过程中,既不需要也不使用
locals()或globals(),避免了信息泄露。 - 为了消除复杂性以及由于递归深度引起的
RuntimeError,不支持递归插值。
然而,字符串字面值中的错误或恶意代码可能会被忽略。虽然代码总体上都可以这么说,但这些表达式位于字符串内部意味着它们更容易被隐藏。
通过工具进行缓解
其想法是,像 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