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)
返回False
operator.exists(NotImplemented)
返回False
operator.exists(Ellipsis)
返回False
float
、complex
和decimal.Decimal
将覆盖存在性检查,使NaN
值返回False
,其他值(包括零值)返回True
- 对于任何其他类型,
operator.exists(obj)
默认返回 True。最重要的是,在真值检查上下文中评估为 False 的值(零、空容器)在存在性检查上下文中仍将评估为 True
PEP 撤回
在 python-ideas [4] 上发布本 PEP 以供讨论时,我要求审阅者在考虑此特定语法提案的细节之前,考虑 3 个高级设计问题
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 None
value2 = expr2["field"]["of"]["interest"] if expr2 is not None else None
value3 = 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.interest
value2 = expr2?["field"]["of"]["interest"]
value3 = expr3 ?else expr4 ?else expr5
在这些形式中,几乎所有呈现给读者的信息都与“这段代码做了什么?”的问题直接相关,而通过将其传递到输出或回退到备用输入来处理丢失数据的样板代码,已经缩减为两个 ?
符号的使用和两个 ?else
关键字的使用。
在前两个示例中,31 个字符的样板条款 if exprN is not None else None
(对于单个字母变量名,最少为 27 个字符)已被单个 ?
字符替换,从而显著提高了代码行的信号噪声比(特别是如果它鼓励使用更有意义的变量和字段名称,而不是为了表达简洁而缩短它们)。
在最后一个示例中,两个 21 个字符的样板条款 if exprN is not None
(最少 17 个字符)被单个字符替换,同样也显著提高了信号噪声比。
此外,我们的 5 个“可能感兴趣的子表达式”中的每一个都只包含一次,而不是 4 个需要重复或提取到一个命名变量中以先检查它们是否存在。
存在检查前提运算符主要定义为为存在检查属性访问和索引运算符提供一个清晰的概念基础
obj?.attr
大致等效于obj ?then obj.attr
obj?[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 ‘THERE EXISTS’:∃
因此,在本 PEP 中提出的语法添加的一种可能方法是使用已经定义的数学符号
expr1 ∃then expr2
expr1 ∃else expr2
obj∃.attr
obj∃[expr]
target ∃= expr
但是,这种方法有两个主要问题,一个是实际问题,另一个是教学问题。
实际问题是,大多数键盘都没有提供除基本算术运算中使用的数学符号以外的任何简单方法(即使本 PEP 中出现的符号最终也是从 [3] 复制粘贴的,而不是直接输入的)。
教学问题是,存在断言 (∃
) 和通用断言 (∀
) 的符号对于大多数人来说并不像基本算术运算符那样熟悉,因此我们实际上并没有通过采用 ∃
使提议的语法更容易理解。
相比之下,?
是 Python 语法中为数不多的剩余未使用的 ASCII 标点符号之一,使其成为“这种控制流操作基于存在检查,而不是真值检查”的候选语法标记。
走这条路还将具有使 Python 语法与提供类似功能的其他语言中的相应语法保持一致的优势。
借鉴 PEP 505 中现有的摘要以及“安全导航运算符 [1] 和“空值合并运算符” [2] 的维基百科文章,我们看到
?.
存在检查属性访问语法与- C# 中的“安全导航”属性访问运算符 (
?.
) - Swift 中的“可选链”运算符 (
?.
) - Groovy 中的“安全导航”属性访问运算符 (
?.
) - Dart 中的“条件成员访问”运算符 (
?.
)
- C# 中的“安全导航”属性访问运算符 (
?[]
存在检查属性访问语法与- C# 中的“安全导航”下标运算符 (
?[]
) - Swift 中的“可选下标”运算符 (
?[].
)
- C# 中的“安全导航”下标运算符 (
?else
存在检查回退语法在语义上与- C# 中的“空值合并”运算符 (
??
) - PHP 中的“空值合并”运算符 (
??
) - Swift 中的“空值合并”运算符 (
??
)
- C# 中的“空值合并”运算符 (
需要明确的是,这些不是其他语言中使用的这些运算符的唯一拼写,但它们是最常见的拼写,而 ?
符号是最常见的语法标记(可能是由于在 C 风格条件表达式中使用 ?
来引入“then”子句,而许多这些语言也提供这种条件表达式)。
建议的关键字
鉴于符号标记 ?
,使用与真值检查对应运算符相同的关键字来拼写存在检查前提和回退运算符在语法上是明确的
expr1 ?and expr2
(而不是expr1 ?then expr2
)expr1 ?or expr2
(而不是expr1 ?else expr2
)
但是,虽然在编写时语法上是明确的,但这种方法使代码难以发音(“?”的发音是什么?)而且难以描述(鉴于重复使用的关键字,没有明显的简写术语来表示“存在检查前提 (?and)”和“存在检查回退 (?or)”来区分它们和“逻辑合取 (and)”和“逻辑析取 (or)”。
我们可以尝试鼓励人们将 ?
符号发音为“exists”,使简写名称为“exists-and 表达式”和“exists-or 表达式”,但无法仅仅从看到它们写在代码中就猜出这些名称。
相反,本 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 环境中作为尾随后缀,以请求前一个表达式的结果的帮助信息。
然而,存在检查的概念确实受益于一个普遍的视觉标记,它将存在检查与真值检查区分开来,如果我们要进行存在检查,则需要一个单字符的符号语法。
概念复杂性
本提案将当前的“存在检查”的临时和非正式概念提升为语法语言特性,并具有明确定义的操作员协议。
在许多方面,这实际上应该减少语言的整体概念复杂性,因为与使用 expr is not None
进行真值检查和存在检查(或在操作数强制转换的上下文中使用 expr is not NotImplemented
,或在数学库中进行各种 NaN 检查操作)相比,使用 bool(expr)
进行真值检查和使用 operator.exists(expr)
进行存在检查之间的映射关系更加正确。
作为此 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
可疑的原因基本相同:前者将评估 default()
,如果 f(arg1)
返回 None
,就像后者将在布尔上下文中评估 expr2
,如果 expr1
评估为 False
。
与条件表达式的模糊交互
在当前编写的提案中,以下是一个语法错误
value = f(arg) if arg ?else default
而以下是一个有效的操作,它在第一个条件不存在时检查第二个条件,而不是仅仅是假
value = expr1 if cond1 ?else cond2 else expr2
上面描述的表达式链接问题意味着,可以这样论证,第一个操作应该等效于
value = f(arg) if operator.exists(arg) else default
要求第二个以 arguably 清晰的形式编写
value = expr1 if (cond1 ?else cond2) else expr2
或者,第一种形式可以保持语法错误,存在检查符号可以附加到 if
关键字
value = expr1 if? cond else expr2
其他真值检查上下文中的存在性检查
真值检查协议当前在以下语法结构中使用
- 逻辑合取(与表达式)
- 逻辑析取(或表达式)
- 条件表达式(if-else 表达式)
- if 语句
- while 循环
- 推导和生成器表达式中的过滤子句
在当前的 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
NaN
值,适用于float
、complex
和decimal.Decimal
省略号
NotImplemented
更改这些的主要论据是,如果我们制定了一个推荐的不变式,说明在存在检查上下文中指示它们不存在的值也应该在真值检查上下文中报告自身为 False
,那么更容易推理潜在的代码行为。
未能定义这样的不变式会导致 arguably 奇特的结局,比如 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
最后修改时间:2023-10-11 12:05:51 GMT