PEP 531 – 存在性检查运算符
- 作者:
- Alyssa Coghlan <ncoghlan at gmail.com>
- 状态:
- 已撤回
- 类型:
- 标准跟踪
- 创建日期:
- 2016-10-25
- Python 版本:
- 3.7
- 发布历史:
- 2016-10-28
摘要
受 PEP 505 及相关讨论的启发,本 PEP 提议向 Python 添加两个新的控制流运算符
- 存在性检查前置条件(“存在即执行”):
expr1 ?then expr2 - 存在性检查回退(“存在即否则”):
expr1 ?else expr2
以及以下常见存在性检查表达式和语句的缩写
- 存在性检查属性访问:
obj?.attr(相当于obj ?then obj.attr) - 存在性检查下标访问:
obj?[expr](相当于obj ?then obj[expr]) - 存在性检查赋值:
value ?= expr(相当于value = value ?else expr)
这些新运算符定义中的常见 ? 符号表示它们使用新的“存在性检查”协议,而不是 if 语句、while 循环、列表推导式、生成器表达式、条件表达式、逻辑与和逻辑或所使用的既有布尔判断协议。
这个新协议将通过 operator.exists 提供,具有以下特征:
- 类型可以定义新的
__exists__特殊方法(Python)或tp_exists插槽(C)来覆盖默认行为。此可选方法具有与__bool__相同的签名和可能的返回值。 operator.exists(None)返回Falseoperator.exists(NotImplemented)返回Falseoperator.exists(Ellipsis)返回Falsefloat、complex和decimal.Decimal将覆盖存在性检查,使得NaN值返回False,而其他值(包括零值)返回True。- 对于任何其他类型,
operator.exists(obj)默认返回 True。最重要的是,在布尔判断语境下为 False 的值(零、空容器)在存在性检查语境下仍为 True。
PEP 撤回
在 python-ideas 邮件列表中发布此 PEP 供讨论时 [4],我要求审阅者在考虑此特定语法提案的细节之前,先考虑三个高层次的设计问题:
1. 我们是否普遍认为“存在性检查”是一个在软件开发中有用且与“布尔判断”概念不同的通用概念? 2. 我们是否普遍认为 Python 生态系统将受益于一种存在性检查协议,该协议允许算法(尤其是短路算法)在不同的“数据缺失”指示符之间进行泛化,包括语言定义、标准库和自定义用户代码中定义的指示符? 3. 如果存在对布尔判断“and”和“or”控制流运算符的存在性检查等效运算符,是否更容易有效地使用这种协议?
虽然对第一个问题的回答普遍是肯定的,但很快就清楚,对第二个问题的回答是“否”。
Steven D’Aprano 在 [5] 中很好地阐述了反驳意见,但总体思想是,在检查“缺失数据”哨兵时,我们几乎总是寻找一个特定的哨兵值,而不是任何哨兵值。
NotImplemented 的存在,例如,是因为 None 可能是重载算术运算符的一个合法结果,而异常处理会产生过高的运行时开销,不足以用于操作数强制转换。
类似地,Ellipsis 是为了支持多维切片而存在的,因为 None 在切片语境中已经有了另一个含义(表示使用默认的开始或停止索引,或默认的步长)。
在数学中,NaN 的价值在于它在编程上表现得像其类型的正常值(例如,公开所有常规属性和方法),而在算术上它则遵循处理 NaN 值的数学规则。
随着核心设计理念的失效,整个提案变得没有意义,因此被撤回。
然而,对该提案的讨论促使考虑一种基于协议的方法,以使现有的 and、or 和 if-else 运算符更加灵活 [6],而无需引入任何新语法,因此我将把它作为 PEP 505 的另一种可能替代方案来撰写。
与其他 PEP 的关系
虽然本 PEP 的灵感和基础是 Mark Haase 在整理 PEP 505 时所做的杰出工作,但由于提案的语法和语义细节存在重大差异,它最终与该 PEP 存在竞争关系。
它还提供了一种不同的视角来解释变革的理由,重点关注对现有 Python 用户的益处,因为应用程序和服务的开发活动中的典型需求正在发生真正的变化。近年来,类似的功能开始出现在多种编程语言中并非偶然,虽然我们应该从其他语言设计者如何处理这个问题中学习,但其他地方树立的先例对于我们如何着手解决这个问题比对于我们是否认为这是一个问题更具相关性。
基本原理
存在性检查表达式
现代软件开发中一个日益普遍的要求是处理“半结构化数据”:即结构已知但运行时可能缺失数据块的数据,并且处理这些数据的软件应能优雅地降级(例如,通过省略依赖于缺失数据的结果),而不是直接失败。
在出现此问题的一些特别常见的情况下包括:
- 处理可选的应用程序配置设置和函数参数
- 处理分布式系统中的外部服务故障
- 处理包含部分记录的数据集
后两种情况是本 PEP 的主要动机——虽然处理可选配置设置和参数的需求至少和 Python 本身一样古老,但公共云基础设施的兴起、软件系统作为分布式服务协作网络的开发,以及可用于分析的大型公共和私有数据集的可用性,意味着在面对部分服务故障或部分数据可用性时能够优雅地降级操作,正成为现代编程环境中的一项基本功能。
目前,在 Python 中编写此类软件可能会非常笨拙,因为您的代码中会充斥着类似这样的表达式:
value1 = expr1.field.of.interest if expr1 is not None else Nonevalue2 = expr2["field"]["of"]["interest"] if expr2 is not None else Nonevalue3 = expr3 if expr3 is not None else expr4 if expr4 is not None else expr5
如果这些情况只是偶尔发生,那么扩展为完整的语句形式可能会有助于提高可读性,但如果您连续遇到 4 或 5 个这样的情况(这在数据转换管道中相当常见),那么用 16 或 20 行条件逻辑替换它们并不能真正解决问题。
扩展以上三个示例希望能说明:
if expr1 is not None:
value1 = expr1.field.of.interest
else:
value1 = None
if expr2 is not None:
value2 = expr2["field"]["of"]["interest"]
else:
value2 = None
if expr3 is not None:
value3 = expr3
else:
if expr4 is not None:
value3 = expr4
else:
value3 = expr5
本 PEP 中提案的组合效果是允许将上述示例表达式写为:
value1 = expr1?.field.of.interestvalue2 = expr2?["field"]["of"]["interest"]value3 = expr3 ?else expr4 ?else expr5
在这些形式中,几乎所有呈现给读者的信息都直接与“这段代码做什么?”这个问题相关,而处理缺失数据(将其传递到输出或回退到备用输入)的样板代码已缩减为对 ? 符号和 ?else 关键字的两次使用。
在前两个示例中,31 个字符的样板子句 if exprN is not None else None(单个字母变量名至少需要 27 个字符)被一个 ? 字符替换,大大提高了行的信号-模式比(尤其是如果它鼓励使用更有意义的变量名和字段名,而不是为了表达式的简洁性而缩短它们)。
在最后一个示例中,21 个字符的样板(至少 17 个字符)的两个实例 if exprN is not None 被单个字符替换,再次大大提高了信号-模式比。
此外,我们“感兴趣的子表达式”中的每一个都只包含一次,而不是其中四个需要重复或提取到命名变量中才能首先检查其是否存在。
存在性检查前置条件运算符主要用于为存在性检查属性访问和下标访问运算符提供清晰的概念基础。
obj?.attr大致相当于obj ?then obj.attrobj?[expr]大致相当于obj ?then obj[expr]
简写形式与其展开等价形式之间的主要语义区别在于,存在性检查运算符左侧的公共子表达式在简写形式中只评估一次(类似于增强赋值语句提供的优势)。
存在性检查赋值
存在性检查赋值被提议为本 PEP 中概念的相对直接的扩展,也涵盖了常见的配置处理惯用法:
value = value if value is not None else expensive_default()
允许其缩写为:
value ?= expensive_default()
这主要在目标是下标操作或子属性时才有利,因为即使没有此特定更改,PEP 仍允许将此惯用法更新为:
value = value ?else expensive_default()
反对添加此形式的主要论点是,它可能存在歧义,并且可能意味着:
value = value ?else expensive_default();或value = value ?then value.subfield.of.interest
第二种形式完全没有用处,但如果此顾虑被认为足够重要需要解决,同时又要保留增强赋值功能,则可以将完整的关键字包含在语法中:
value ?else= expensive_default()
或者,增强赋值可以完全从当前提案中删除,并可能在以后重新考虑。
存在性检查协议
存在性检查协议包含在本提案中,主要是为了允许代理对象(例如远程资源的本地表示)和测试中使用的模拟对象正确地指示目标资源不存在,即使代理或模拟对象本身不是 None。
然而,一旦定义了该协议,似乎很自然地将其扩展为提供一种独立于类型的 NaN 值检查方法——目前,您需要了解正在处理的具体数据类型(例如,内置浮点数、内置复数、decimal 模块)并使用适当的操作(例如,分别是 math.isnan、cmath.isnan、decimal.getcontext().is_nan())。
类似地,声明其他占位符内置单例 Ellipsis 和 NotImplemented 也符合表示数据缺失而非数据的对象,这是合理的。
建议的符号表示法
Python 历史上只有一种隐含的布尔语境:布尔判断,可以通过 `bool()` 内置函数直接调用。由于本 PEP 提出了一种基于存在性检查而非布尔判断的新型控制流操作,因此有价值的是在代码中直接提醒存在性检查的使用,而不是布尔判断。
存在性断言的数学符号是 U+2203 “存在”: ∃
因此,本 PEP 提出的语法添加的一种可能方法是使用该已定义的数学符号:
expr1 ∃then expr2expr1 ∃else expr2obj∃.attrobj∃[expr]target ∃= expr
然而,这种方法有两个主要问题:一个实际问题,一个教学问题。
实际问题是,大多数键盘除了基本的算术符号外,没有提供方便输入数学符号的方法(即使本 PEP 中出现的符号最终也是从 [3] 复制粘贴的,而不是直接输入的)。
教学问题是,存在性断言(∃)和全称断言(∀)的符号对于大多数人来说并不熟悉,就像基本的算术运算符一样,因此通过采用 ∃ 我们实际上并没有让拟议的语法更容易理解。
相比之下,? 是 Python 语法中为数不多的几个未使用的 ASCII 标点符号之一,可以作为“此控制流操作基于存在性检查,而非布尔判断”的候选语法标记。
选择这条路径还将使 Python 的语法与其他提供类似功能的语言相对应,从而具有优势。
借鉴 PEP 505 中的现有摘要以及关于“安全导航运算符 [1] 和“空值合并运算符”[2] 的维基百科文章,我们看到:
?.存在性检查属性访问语法与以下内容精确对齐:- C# 中的“安全导航”属性访问运算符(
?.) - Swift 中的“可选链式调用”运算符(
?.) - Groovy 中的“安全导航”属性访问运算符(
?.) - Dart 中的“条件成员访问”运算符(
?.)
- C# 中的“安全导航”属性访问运算符(
?[]存在性检查属性访问语法与以下内容精确对齐:- C# 中的“安全导航”下标运算符(
?[]) - Swift 中的“可选下标”运算符(
?[].)
- C# 中的“安全导航”下标运算符(
?else存在性检查回退语法在语义上与以下内容对齐:- C# 中的“空值合并”运算符(
??) - PHP 中的“空值合并”运算符(
??) - Swift 中的“nil 合并”运算符(
??)
- C# 中的“空值合并”运算符(
明确地说,这些并非其他语言中这些运算符的唯一拼写方式,但它们是最常见的,而 ? 符号是迄今为止最常见的语法标记(可能受到 C 风格条件表达式中 ? 用于引入“then”子句的启发,而这些语言中的许多也提供这种条件表达式)。
建议的关键字
给定符号标记 ?,使用与其布尔判断对应项相同的关键字来拼写存在性检查前置条件和回退操作在语法上是明确的:
expr1 ?and expr2(代替expr1 ?then expr2)expr1 ?or expr2(代替expr1 ?else expr2)
然而,虽然书写时语法明确,但这种方法使得代码非常难于发音(“?” 的发音是什么?)并且也难于描述(鉴于重复使用的关键字,没有明显的简写术语来区分“存在性检查前置条件 (?and)”和“存在性检查回退 (?or)”与“逻辑与 (and)”和“逻辑或 (or)”)。
我们可以尝试鼓励人们将 ? 符号读作“exists”,使简写名称成为“exists-and expression”和“exists-or expression”,但仅从代码中看到它们就无法猜到这些名称。
相反,本 PEP 利用了提议的符号语法,引入了一个新关键字(?then)并借用了一个现有关键字(?else),以便人们能够无歧义地指代“then 表达式”和“else 表达式”。
这些关键字也与语义上等同于拟议表达式的条件表达式很好地对齐。
对于 ?else 表达式,expr1 ?else expr2 等同于:
_lhs_result = expr1
_lhs_result if operator.exists(_lhs_result) else expr2
在这里,平行关系很清晰,因为 else expr2 出现在缩写形式和展开形式的末尾。
对于 ?then 表达式,expr1 ?then expr2 等同于:
_lhs_result = expr1
expr2 if operator.exists(_lhs_result) else _lhs_result
这里的平行关系并不明显,因为 Python 传统上使用匿名“then”子句(在 `if` 语句中由 `:` 引入,在条件表达式中由 `if` 结尾),但只要您已经熟悉“if-then-else”对条件控制流的解释,它仍然是相当清晰的。
风险和顾虑
可读性
有效地学习阅读和编写新语法主要需要内化两个概念:
- 包含
?的表达式包含存在性检查,并且可能会短路。 - 如果
None或其他“不存在”的值是预期的输入,并且正确的处理方式是将其传播到结果,那么存在性检查运算符很可能是您想要的。
目前,这些概念在语言层面没有明确表示,因此需要根据条件表达式和语句的各种惯用模式来学习识别和使用它们。
魔法语法
作为语法元素,? 本身并不会自动暗示 is not None 或 operator.exists。目前 Python 代码中 `?` 符号的主要用法是作为 IPython 环境中的后缀,用于请求表达式结果的帮助信息。
然而,存在性检查的概念确实受益于一个普遍的视觉标记,以将其与布尔判断区分开来,如果我们要这样做,就需要一个单字符的符号语法。
概念复杂性
本提案将目前临时和非正式的“存在性检查”概念提升为一种语法语言特性,具有明确定义的运算符协议。
在许多方面,这实际上应该减少语言的总体概念复杂性,因为更多的期望将能正确地映射于 `bool(expr)` 的布尔判断与 `operator.exists(expr)` 的存在性检查之间,而不是目前布尔判断与 `expr is not None`(或操作数强制转换语境下的 `expr is not NotImplemented`,或数学库中的各种 NaN 检查操作)的存在性检查之间的映射。
作为本 PEP 引入的新并行关系的一个简单示例,请比较:
all_are_true = all(map(bool, iterable))
at_least_one_is_true = any(map(bool, iterable))
all_exist = all(map(operator.exists, iterable))
at_least_one_exists = any(map(operator.exists, iterable))
设计讨论
存在性检查表达式链的细微差别
在链接存在性检查表达式时会出现与链接逻辑运算符中已有的相似的细微差别:如果链中某个表达式的右侧本身返回一个不存在的值,其行为可能会令人惊讶。
因此,value = arg1 ?then f(arg1) ?else default() 的有效性与 value = cond and expr1 or expr2 的有效性差的理由基本相同:前者会在 `f(arg1)` 返回 None 时评估 default(),就像后者会在 `expr1` 在布尔语境下评估为 False 时评估 expr2 一样。
与条件表达式的交互歧义
在当前 PEP 中,以下写法是语法错误:
value = f(arg) if arg ?else default
而以下写法是有效操作,它在第一个条件不存在时检查第二个条件,而不仅仅是判断其是否为 False:
value = expr1 if cond1 ?else cond2 else expr2
上述表达式链接问题意味着,可以论证第一种操作应该等同于:
value = f(arg) if operator.exists(arg) else default
要求第二种写法采用可能更清晰的形式:
value = expr1 if (cond1 ?else cond2) else expr2
或者,第一种形式可以保持语法错误,然后将存在性检查符号附加到 if 关键字上:
value = expr1 if? cond else expr2
其他布尔判断语境下的存在性检查
目前,布尔判断协议用于以下语法结构:
- 逻辑与(and 表达式)
- 逻辑或(or 表达式)
- 条件表达式(if-else 表达式)
- if 语句
- while 循环
- 列表推导式和生成器表达式中的 filter 子句
在当前 PEP 中,将 `and` 和 `or` 的布尔判断切换为存在性检查,只需要在适当的位置替换为新的关键字 ?then 和 ?else。
对于其他布尔判断语境,它建议要么导入并使用 operator.exists API,要么继续使用当前惯用法,即检查特定于 expr is not None(或语境适用的等价物)。
在这方面最简单的增强是,将提议的 exists() API 从运算符模块函数提升为新的内置函数。
或者,? 存在性检查符号可以作为 if 和 while 关键字的修饰符,以指示使用存在性检查而不是布尔判断。
然而,目前尚不清楚上述任一建议所带来的潜在一致性好处是否能证明额外的干扰是合理的,因此它们目前已从提案中省略。
定义 __bool__ 和 __exists__ 之间预期的不变关系
PEP 目前保留了所有现有类型的 __bool__ 定义不变,这确保了整个提案向后兼容,但导致了以下情况:bool(obj) 返回 True,但提议的 operator.exists(obj) 将返回 False:
float、complex和decimal.Decimal的NaN值EllipsisNotImplemented
更改这些值的主要论点是,当存在一个推荐的不变式,表明在存在性检查语境下表示不存在的值在布尔判断语境下也应报告为 False 时,可以更容易地推断代码行为。
未能定义这样一个不变式会导致一些可能令人费解的结果,例如 float("NaN") ?else 0.0 返回 0.0,而 float("NaN") or 0.0 返回 NaN。
限制
任意的哨兵对象
本提案不尝试为“哨兵对象”惯用法提供语法支持,其中 None 是允许的显式值,因此定义了一个单独的哨兵对象来指示缺失值。
_SENTINEL = object()
def f(obj=_SENTINEL):
return obj if obj is not _SENTINEL else default_value()
这可能会以牺牲存在协议定义的显著复杂性为代价(包括定义和使用),从而得到支持。
- 在 Python 层,
operator.exists和__exists__实现将返回空元组以表示不存在,否则返回一个包含对象引用的单例元组,作为存在性检查的结果。 - 在 C 层,
tp_exists实现将返回 NULL 以表示不存在,否则返回一个PyObject *指针作为存在性检查的结果。
有了这个改变,哨兵对象惯用法可以重写为:
class Maybe:
SENTINEL = object()
def __init__(self, value):
self._result = (value,) is value is not self.SENTINEL else ()
def __exists__(self):
return self._result
def f(obj=Maybe.SENTINEL):
return Maybe(obj) ?else default_value()
然而,我不认为不能使用 3 个提议的标准哨兵值(即 None、Ellipsis 和 NotImplemented)的情况会足够普遍,以至于增加协议复杂性和 __bool__ 与 __exists__ 之间的对称性损失是值得的。
规范
摘要已经给出了提案的要点,理由也提供了一些具体的例子。如果对基本思想有足够兴趣,那么完整的规范将需要提供拟议语法糖与底层条件表达式之间的精确对应关系,以指导参考实现的创建。
……待定……
实施
与 PEP 505 一样,实际实现已推迟,等待对添加这些运算符的想法的原则性兴趣——实现不是这些提案的难点,难点在于决定这是一个长期效益(对新老 Python 用户)超过短期成本(包括其他实现开发者、语言课程开发者以及其他 Python 相关教育材料的作者适应该变化)的变化。
……待定……
参考资料
版权
本文件已根据 CC0 1.0 许可协议的条款置于公共领域:https://creativecommons.org/publicdomain/zero/1.0/
来源:https://github.com/python/peps/blob/main/peps/pep-0531.rst