Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 532 – 断路器协议和二元运算符

作者:
Alyssa Coghlan <ncoghlan at gmail.com>, Mark E. Haase <mehaase at gmail.com>
状态:
延期
类型:
标准轨道
创建:
2016-10-30
Python 版本:
3.8
历史记录:
2016-11-05

目录

PEP 延期

对该 PEP 的进一步考虑已推迟到 Python 3.8 或之后。

摘要

PEP 335PEP 505PEP 531 以及相关讨论的启发,该 PEP 提出定义一个新的断路器协议(使用方法名称 __then____else__),为以下内容提供通用的底层语义基础

  • 条件表达式: LHS if COND else RHS
  • 逻辑与: LHS and RHS
  • 逻辑或: LHS or RHS
  • PEP 505 中提出的无关 None 的运算符
  • PEP 535 中提出的富比较链式模型

利用新的协议,它进一步建议修改条件表达式的定义,以允许使用 ifelse 作为右结合和左结合通用短路运算符

  • 右结合短路: LHS if RHS
  • 左结合短路: LHS else RHS

为了使逻辑否定 (not EXPR) 与上述更改一致,它还建议引入一个新的逻辑否定协议(使用方法名称 __not__)。

为了在不评估创建断路器的表达式两次的情况下强制断路器短路,将向 operator 模块添加一个新的 operator.short_circuit(obj) 辅助函数。

最后,提出了一个新的标准 types.CircuitBreaker 类型,以将对象的真值(用于确定控制流)与其从短路断路器表达式中返回的值分离,并向 operator 模块添加以下工厂函数以表示特别常见的切换习惯用法

  • 切换 bool(obj): operator.true(obj)
  • 切换 not bool(obj): operator.false(obj)
  • 切换 obj is value: operator.is_sentinel(obj, value)
  • 切换 obj is not value: operator.is_not_sentinel(obj, value)

与其他 PEP 的关系

该 PEP 建立在其他提案中的一系列工作之上。以下是其中一些关键提案的讨论。

PEP 531: 存在性检查协议

该 PEP 是 PEP 531 的直接继任者,用新的断路器协议和对条件表达式以及 not 运算符的调整替换了其中定义的存在性检查协议以及新的 ?then?else 语法运算符。

PEP 505: 无关 None 的运算符

该 PEP 补充了 PEP 505 中的无关 None 的运算符提案,它提供了一个基于协议驱动的语义框架,将它们的短路行为解释为针对条件表达式特定使用的高度优化的语法糖。

鉴于该 PEP 中提出的更改

  • LHS ?? RHS 粗略地说,就是 is_not_sentinel(LHS, None) else RHS
  • EXPR?.attr 粗略地说,就是 EXPR.attr if is_not_sentinel(EXPR, None)
  • EXPR?[key] 粗略地说,就是 EXPR[key] if is_not_sentinel(EXPR, None)

在这三种情况下,专用语法形式都将被优化,以避免实际创建断路器实例,而是直接实现底层控制流。在后两种情况下,语法形式也将避免对 EXPR 进行两次评估。

这意味着,虽然无关 None 的运算符将仍然高度专业化并且特定于 None,但其他哨兵值仍然可以通过该 PEP 中更通用的协议驱动提案使用。

PEP 335: 可重载的布尔运算符

PEP 335 提出了直接重载短路 andor 运算符的能力,重载比较链式的语义能力是该更改的其中一项结果。该 PEP 早期版本中有关通过更改比较链式的语义定义来处理逐元素比较用例的提案直接来自 Guido 对 PEP 335 的拒绝 [1]

但是,对该 PEP 的初步反馈表明,它涵盖的提案数量太多,导致难以阅读,因此该提案的这部分已被分离出来,成为 PEP 535

PEP 535: 富比较链式

如上所述,PEP 535 是一项提案,旨在建立在该 PEP 中定义的断路器协议之上,以扩展 PEP 207 中引入的富比较支持,以处理诸如 LEFT_BOUND < VALUE < RIGHT_BOUND 之类的比较链式操作。

规范

断路器协议 (if-else)

条件表达式 (LHS if COND else RHS) 当前被解释为表达式级别等效于

if COND:
    _expr_result = LHS
else:
    _expr_result = RHS

该 PEP 建议更改该扩展,以允许检查的条件实现一个新的“断路器”协议,该协议允许它查看并可能更改表达式的任一或两个分支的结果

_cb = COND
_type_cb = type(cb)
if _cb:
    _expr_result = LHS
    if hasattr(_type_cb, "__then__"):
        _expr_result = _type_cb.__then__(_cb, _expr_result)
else:
    _expr_result = RHS
    if hasattr(_type_cb, "__else__"):
        _expr_result = _type_cb.__else__(_cb, _expr_result)

如上所示,解释器实现将需要仅访问条件表达式实际执行的分支所需的协议方法。与其他协议方法一致,特殊方法将通过断路器的类型进行查找,而不是直接在实例上进行查找。

断路器运算符(二元 if 和二元 else)

该协议的建议名称并非来自对条件表达式语义的建议更改。相反,它来自建议添加 ifelse 作为通用协议驱动的短路运算符,以补充现有的基于 TrueFalse 的短路运算符(分别为 orand),以及 PEP 505 中提出的基于 None 的短路运算符 (??)。

这两个运算符将被称为断路器运算符。

为了支持这种用法,语言语法的条件表达式定义将被更新,以使 if 子句和 else 子句都是可选的

test: else_test ['if' or_test ['else' test]] | lambdef
else_test: or_test ['else' test]

请注意,我们需要避免对 else_test ('if' else_test)* 进行明显的简化,以便编译器实现能够更轻松地正确保留普通条件表达式的语义。

语法的 test_nocond 节点的定义(它有意排除条件表达式)将保持不变,因此断路器运算符在列表推导和生成器表达式的 if 子句中使用时需要括号,就像条件表达式本身一样。

此语法定义意味着优先级/关联性,在expr1 if cond else expr2 else expr3这种模棱两可的情况下,解析为(expr1 if cond else expr2) else epxr3。但是,PEP 8 中也会添加一项指南,说明“不要这样做”,因为这种结构对于读者来说天生会让人迷惑,无论解释器如何执行它。

右关联断路运算符(LHS if RHS)将扩展如下

_cb = RHS
_expr_result = LHS if _cb else _cb

而左关联断路运算符(LHS else RHS)将扩展为

_cb = LHS
_expr_result = _cb if _cb else RHS

需要注意的关键是,在这两种情况下,当断路表达式发生短路时,条件表达式将用作表达式的结果,除非条件本身是一个断路器。在后一种情况下,通常会调用适当的断路器协议方法,但断路器本身将作为方法参数提供。

这允许断路器通过检查作为候选表达式结果传入的参数是否为 self 来可靠地检测短路。

重载逻辑否定 (not)

任何断路器定义都将有一个逻辑反转,它仍然是一个断路器,但会反转何时短路表达式评估的答案。例如,本 PEP 中提出的 operator.trueoperator.false 断路器是彼此的逻辑反转。

将引入一个新的协议方法 __not__(self),允许断路器和其他类型重写 not 表达式,以返回它们的逻辑反转,而不是强制的布尔值结果。

为了保留现有语言优化的语义(例如,在布尔值上下文中直接消除双重否定作为冗余),__not__ 实现将需要遵守以下不变式

assert not bool(obj) == bool(not obj)

但是,对称断路器(那些实现所有 __bool____not____then____else__ 的断路器)仅应在表达式中涉及的所有断路器都使用一致的“真值”定义时才遵守布尔逻辑的全部语义。这将在遵守 De Morgan 定律中进一步说明。

强制短路行为

可以使用断路器作为条件表达式中的所有三个操作数来强制调用断路器的短路行为

obj if obj else obj

或者,等效地,作为断路表达式中的两个操作数

obj if obj
obj else obj

本 PEP 建议在 operator 中添加一个专用函数,而不是要求使用任何这些模式,以明确地短路断路器,同时将其他对象直接传递

def short_circuit(obj)
    """Replace circuit breakers with their short-circuited result

    Passes other input values through unmodified.
    """
    return obj if obj else obj

断路器身份比较 (isis not)

在没有标准断路器的情况下,提议的 ifelse 运算符很大程度上只是现有 andor 逻辑运算符的不同拼写。

但是,本 PEP 进一步建议提供一个新的通用 types.CircuitBreaker 类型,它实现了适当的短路逻辑,以及在操作符模块中与 isis not 运算符相对应的工厂函数。

这些将以这样的方式定义,当条件检查失败时,以下表达式会生成 VALUE 而不是 False

EXPR if is_sentinel(VALUE, SENTINEL)
EXPR if is_not_sentinel(VALUE, SENTINEL)

类似地,当条件检查成功时,这些将生成 VALUE 而不是 True

is_sentinel(VALUE, SENTINEL) else EXPR
is_not_sentinel(VALUE, SENTINEL) else EXPR

实际上,这些比较将被定义为以下形式的表达式的开头 VALUE if 和结尾 else VALUE 子句可以省略

# To handle "if" expressions, " else VALUE" is implied when omitted
EXPR if is_sentinel(VALUE, SENTINEL) else VALUE
EXPR if is_not_sentinel(VALUE, SENTINEL) else VALUE
# To handle "else" expressions, "VALUE if " is implied when omitted
VALUE if is_sentinel(VALUE, SENTINEL) else EXPR
VALUE if is_not_sentinel(VALUE, SENTINEL) else EXPR

提议的 types.CircuitBreaker 类型将以编程方式表示此行为,如下所示

class CircuitBreaker:
    """Simple circuit breaker type"""
    def __init__(self, value, bool_value):
        self.value = value
        self.bool_value = bool(bool_value)
    def __bool__(self):
        return self.bool_value
    def __not__(self):
        return CircuitBreaker(self.value, not self.bool_value)
    def __then__(self, result):
        if result is self:
            return self.value
        return result
    def __else__(self, result):
        if result is self:
            return self.value
        return result

这些断路器的关键特征是它们是短暂的:当它们被告知短路已发生(通过接收对自身作为候选表达式结果的引用)时,它们会返回原始值,而不是断路包装器。

短路检测定义为,如果显式将相同的断路器实例传递给断路运算符的两侧或将其用作条件表达式中的所有三个操作数,则包装器将始终被删除

breaker = types.CircuitBreaker(foo, foo is None)
assert operator.short_circuit(breaker) is foo
assert (breaker if breaker) is foo
assert (breaker else breaker) is foo
assert (breaker if breaker else breaker) is foo
breaker = types.CircuitBreaker(foo, foo is not None)
assert operator.short_circuit(breaker) is foo
assert (breaker if breaker) is foo
assert (breaker else breaker) is foo
assert (breaker if breaker else breaker) is foo

然后,operator 模块中的工厂函数使创建对应于使用 isis not 运算符进行身份检查的断路器变得简单

def is_sentinel(value, sentinel):
    """Returns a circuit breaker switching on 'value is sentinel'"""
    return types.CircuitBreaker(value, value is sentinel)

def is_not_sentinel(value, sentinel):
    """Returns a circuit breaker switching on 'value is not sentinel'"""
    return types.CircuitBreaker(value, value is not sentinel)

真值检查比较

由于它们的短路性质,andor 运算符背后的运行时逻辑以前从未通过 operatortypes 模块访问过。

引入断路运算符和断路器允许将该逻辑捕获在操作符模块中,如下所示

def true(value):
    """Returns a circuit breaker switching on 'bool(value)'"""
    return types.CircuitBreaker(value, bool(value))

def false(value):
    """Returns a circuit breaker switching on 'not bool(value)'"""
    return types.CircuitBreaker(value, not bool(value))
  • LHS or RHS 实际上将是 true(LHS) else RHS
  • LHS and RHS 实际上将是 false(LHS) else RHS

这些运算符定义不会发生任何实际变化,新的断路协议和运算符只会提供一种方法,使控制流逻辑可编程,而不是在开发时硬编码检查的意义。

遵守布尔逻辑规则,这些表达式也可以通过使用右关联断路运算符以其反转形式扩展

  • LHS or RHS 实际上将是 RHS if false(LHS)
  • LHS and RHS 实际上将是 RHS if true(LHS)

无关 None 的运算符

如果本 PEP 和PEP 505 的 None 感知运算符都被接受,那么提议的 is_sentinelis_not_sentinel 断路器工厂将用于封装“None 检查”的概念:查看一个值是否为 None 以及是回退到备用值(称为“None 合并”)还是将其作为整体表达式的结果传递(称为“None 切断”或“None 传播”)。

鉴于这些断路器,LHS ?? RHS 大致相当于以下两种

  • is_not_sentinel(LHS, None) else RHS
  • RHS if is_sentinel(LHS, None)

由于它们将控制流注入属性查找和下标操作的方式,因此无法直接使用断路运算符来表达 None 感知属性访问和 None 感知下标,但它们仍然可以使用底层的断路协议来定义。

用这些术语,EXPR?.ATTR[KEY].SUBATTR() 在语义上等效于

_lookup_base = EXPR
_circuit_breaker = is_not_sentinel(_lookup_base, None)
_expr_result = _lookup_base.ATTR[KEY].SUBATTR() if _circuit_breaker

类似地,EXPR?[KEY].ATTR.SUBATTR() 在语义上等效于

_lookup_base = EXPR
_circuit_breaker = is_not_sentinel(_lookup_base, None)
_expr_result = _lookup_base[KEY].ATTR.SUBATTR() if _circuit_breaker

None 感知运算符的实际实现可能会被优化以跳过实际创建断路器实例,但上述扩展仍然可以准确地描述运算符在运行时的可观察行为。

富比较链式

有关此可能用例的详细讨论,请参阅PEP 535

其他条件结构

建议不要对 if 语句、while 语句、推导式或生成器表达式进行任何更改,因为它们包含的布尔值子句完全用于控制流目的,并且从不返回任何结果。

但是,值得注意的是,虽然这些提案不在本 PEP 的范围之内,但这里定义的断路协议已经足以支持诸如

def is_not_none(obj):
    return is_sentinel(obj, None)

while is_not_none(dynamic_query()) as result:
    ... # Code using result

if is_not_none(re.search(pattern, text)) as match:
    ... # Code using match

这可以通过将 operator.short_circuit(CONDITION) 的结果分配给 as 子句中给定的名称来完成,而不是将 CONDITION 直接分配给给定的名称。

样式指南建议

与本 PEP 引入的新功能相关的 PEP 8 的以下补充建议

  • 避免在单个表达式中组合条件表达式(if-else)和独立的断路运算符(ifelse) - 根据情况使用其中之一,而不是两者。
  • 避免在 if 语句中以及推导式和生成器表达式的过滤器子句中使用条件表达式(if-else)和独立的断路运算符(ifelse)作为 if 条件的一部分。

基本原理

添加新运算符

PEP 335类似,本 PEP 的早期草案侧重于使现有的 andor 运算符在解释方面不那么严格,而不是提出新的运算符。但是,事实证明这存在一些关键问题

  • andor 运算符具有悠久的历史,并且含义稳定,因此如果它们的含义现在依赖于左操作数的类型,读者肯定会感到惊讶。即使是新用户也会对这种变化感到困惑,因为 25 年以上的教学材料都假设了这些运算符当前众所周知的语义
  • 包括 CPython 在内的 Python 解释器实现,在定义运行时和编译时优化时,利用了 andor 现有的语义,如果这些操作的语义发生变化,所有这些优化都需要重新审查,并可能需要放弃。
  • 目前尚不清楚为定义协议所需的新方法选择哪些名称合适。

建议使用现有 if-else 三元运算符的短路二元变体来解决所有这些问题。

  • andor 的运行时语义完全保持不变。
  • 虽然一元 not 运算符的语义发生了变化,但对 __not__ 实现的要求不变,这意味着布尔上下文中的现有表达式优化将保持有效。
  • __else__if 表达式的短路结果,因为没有尾随的 else 子句。
  • __then__else 表达式的短路结果,因为没有前导的 if 子句(如果方法名是 __if__,这种联系会更加清楚,但鉴于其他使用不会调用断路协议的 if 关键字,这将是模棱两可的)。

运算符和协议的命名

“断路运算符”、“断路协议”和“断路器”这些名称都受到短路运算符一词的启发:这是用于描述仅在特定条件下才计算其右操作数的运算符的一般语言设计术语。

在电气类比中,Python 中的断路器会在表达式发生短路之前检测和处理短路,类似于断路器在电气系统中发生短路之前检测和处理短路,从而避免损坏设备或伤害人员。

在 Python 层面的类比中,就像 break 语句可以在循环到达自然结束之前终止循环一样,断路表达式可以终止表达式的计算并立即产生结果。

使用现有关键字

使用现有关键字的好处是,可以在没有 __future__ 语句的情况下引入新的运算符。

ifelse 在语义上适合所提议的新协议,并且引入的唯一额外的语法歧义出现在新的运算符与显式的 if-else 条件表达式语法组合使用时。

PEP 通过明确规定解释器实现者应该如何处理这种歧义来解决这种歧义,但建议在 PEP 8 中指出,尽管解释器能够理解它,但人类读者可能无法理解,因此在单个表达式中同时使用条件表达式和断路运算符不是一个好主意。

协议方法的命名

命名 __else__ 方法很简单,因为重用运算符关键字名称会得到一个既明显又明确的特殊方法名称。

命名 __then__ 方法就没那么简单了,因为还有一个使用关键字名称 __if__ 的选择。

使用 __if__ 的问题是,仍然会有很多情况,if 关键字出现在表达式右侧,但不会调用 __if__ 特殊方法。相反,会调用 bool() 内建函数及其底层特殊方法(__bool____len__),而 __if__ 不会有任何作用。

由于布尔协议已在条件表达式和新的断路协议中发挥作用,因此选择了更明确的名称 __then__,该名称基于计算机科学和编程语言设计中通常用来描述 if 语句的第一条子句的术语。

使二元 if 右结合

条件表达式设置的先例意味着,二元短路 if 表达式必须将条件放在右侧,以保证一致性。

由于右操作数始终先计算,如果右操作数在布尔上下文中为真,则左操作数根本不会被计算,因此自然的结果是右结合运算符。

标准断路器的命名

仅与左结合断路运算符一起使用时,如果一元检查的显式断路器名称以介词 if_ 开头,则读起来很好。

operator.if_true(LHS) else RHS
operator.if_false(LHS) else RHS

但是,在执行逻辑反转时,包含 if_ 的读起来就不那么好了。

not operator.if_true(LHS) else RHS
not operator.if_false(LHS) else RHS

或者使用右结合断路运算符时。

LHS if operator.if_true(RHS)
LHS if operator.if_false(RHS)

或者命名二元比较运算时。

operator.if_is_sentinel(VALUE, SENTINEL) else EXPR
operator.if_is_not_sentinel(VALUE, SENTINEL) else EXPR

相比之下,从断路器名称中省略介词,可以得到一个在所有形式的一元检查中读起来都合理的名称。

operator.true(LHS) else RHS       # Preceding "LHS if " implied
operator.false(LHS) else RHS      # Preceding "LHS if " implied
not operator.true(LHS) else RHS   # Preceding "LHS if " implied
not operator.false(LHS) else RHS  # Preceding "LHS if " implied
LHS if operator.true(RHS)         # Trailing " else RHS" implied
LHS if operator.false(RHS)        # Trailing " else RHS" implied
LHS if not operator.true(RHS)     # Trailing " else RHS" implied
LHS if not operator.false(RHS)    # Trailing " else RHS" implied

并且对于二元检查也读起来很好。

operator.is_sentinel(VALUE, SENTINEL) else EXPR
operator.is_not_sentinel(VALUE, SENTINEL) else EXPR
EXPR if operator.is_sentinel(VALUE, SENTINEL)
EXPR if operator.is_not_sentinel(VALUE, SENTINEL)

风险和问题

这个 PEP 的设计初衷是专门解决在讨论 PEP 335、505 和 531 时提出的风险和担忧。

  • 它定义了新的运算符,并调整了链式比较的定义(在单独的 PEP 中),而不是影响现有的 andor 运算符。
  • 所提议的新的运算符是通用的短路二元运算符,甚至可以用来表达 andor 的现有语义,而不是仅仅僵硬地关注与 None 的身份检查。
  • 对一元 not 运算符和二元比较运算符 isis not 的更改的定义方式使得基于现有语义的控制流优化仍然有效。

这种方法的一个结果是,除了使省略一些常见的 None if 前缀和 else None 后缀成为可能之外,这个 PEP 本身没有为最终用户带来多少直接的好处。

相反,它主要提供了一个共同的基础,允许在 PEP 505 中提出的 None 感知运算符提案和 PEP 535 中提出的丰富比较链式提案,在共同的底层语义框架之上进行,该框架也与条件表达式和现有的 andor 运算符共享。

设计讨论

协议演练

下图说明了断路协议背后的核心概念(尽管它忽略了通过类型而不是实例查找特殊方法的细节)。

diagram of circuit breaking protocol applied to ternary expression

我们将遍历以下表达式。

>>> def is_not_none(obj):
...     return operator.is_not_sentinel(obj, None)
>>> x if is_not_none(data.get("key")) else y

is_not_none 是一个辅助函数,它使用 None 作为哨兵值调用提议的 operator.is_not_sentinel types.CircuitBreaker 工厂。 data 是一个容器(例如内置的 dict 实例),当 get() 方法使用未知键调用时,它会返回 None

我们可以重写示例,为断路器实例命名。

>>> maybe_value = is_not_none(data.get("key"))
>>> x if maybe_value else y

这里,maybe_value 断路器实例对应于图中的 breaker

三元条件通过调用 bool(maybe_value) 来计算,这与 Python 现有的行为相同。行为上的变化是,断路协议不是直接返回操作数 xy 之一,而是将相关操作数传递给条件中使用的断路器。

如果 bool(maybe_value) 计算结果为 True(即请求的键存在且其值不是 None),则解释器将调用 type(maybe_value).__then__(maybe_value, x)。否则,它将调用 type(maybe_value).__else__(maybe_value, y)

该协议也适用于新的 ifelse 二元运算符,但在这些情况下,解释器需要一种方法来指示缺少的第三个操作数。它通过在该角色中重复使用断路器本身来实现这一点。

考虑以下两个表达式。

>>> x if data.get("key") is None
>>> x if operator.is_sentinel(data.get("key"), None)

这个表达式的第一种形式在 data.get("key") is None 的情况下返回 x,但在其他情况下返回 False,这几乎肯定不是我们想要的。

相反,这个表达式的第二种形式在 data.get("key") is None 的情况下仍然返回 x,但在其他情况下返回 data.get("key"),这是更有用的行为。

我们可以通过将其重写为具有显式命名的断路器实例的三元表达式来理解这种行为。

>>> maybe_value = operator.is_sentinel(data.get("key"), None)
>>> x if maybe_value else maybe_value

如果 bool(maybe_value)True(即 data.get("key")None),那么解释器将调用 type(maybe_value).__then__(maybe_value, x)types.CircuitBreaker.__then__ 的实现没有看到任何表明短路已经发生的迹象,因此返回 x

相反,如果 bool(maybe_value)False(即 data.get("key") None),那么解释器将调用 type(maybe_value).__else__(maybe_value, maybe_value)types.CircuitBreaker.__else__ 的实现检测到实例方法接收了自身作为其参数,并返回包装的值(即 data.get("key")),而不是断路器。

相同的逻辑适用于 else,只是方向相反。

>>> is_not_none(data.get("key")) else y

如果 data.get("key") 不为 None,那么此表达式将返回 data.get("key"),否则它将计算并返回 y。为了理解其机制,我们将表达式重写如下:

>>> maybe_value = is_not_none(data.get("key"))
>>> maybe_value if maybe_value else y

如果 bool(maybe_value)True,那么表达式将短路,解释器将调用 type(maybe_value).__else__(maybe_value, maybe_value)types.CircuitBreaker.__then__ 的实现检测到实例方法接收了自身作为其参数,并返回包装的值(即 data.get("key")),而不是断路器。

如果 bool(maybe_value)True,解释器将调用 type(maybe_value).__else__(maybe_value, y)types.CircuitBreaker.__else__ 的实现没有看到任何表明短路已经发生的迹象,因此返回 y

尊重 De Morgan 定律

andor 类似,二元短路运算符将允许以多种方式编写本质上相同的表达式。这种看似冗余是将协议定义为完整布尔代数的必然结果,因为布尔代数遵守被称为“德摩根定律”的一对性质:能够用彼此以及 not 运算符的适当组合来表达 andor 运算符的结果。

对于 Python 中的 andor,这些不变式可以描述如下:

assert bool(A and B) == bool(not (not A or not B))
assert bool(A or B) == bool(not (not A and not B))

也就是说,如果你取其中一个运算符,反转两个操作数,切换到另一个运算符,然后反转整个结果,你将得到与原始运算符相同的答案(从布尔意义上讲)。(这可能看起来是多余的,但在许多情况下,它实际上可以让你消除双重否定并找到真值或假值子表达式,从而减小整个表达式的尺寸)。

对于断路器,定义合适的不变式很复杂,因为它们通常被设计为在短路时从表达式结果中消除自身,这是一种固有的不对称行为。因此,在将德摩根定律映射到对称断路器的预期行为时,需要考虑这种固有的不对称性。

解决这种复杂性的一种方法是将原本会短路的操作数包装在 operator.true 中,确保当将 bool 应用于整个结果时,它使用与用于决定要评估哪个分支相同的真值定义,而不是直接将 bool 应用于断路器的输入值。

具体来说,对于新的短路运算符,以下属性在任何行为良好的对称断路器(实现了 __bool____not__)的情况下都应合理地预期为真:

assert bool(B if true(A)) == bool(not (true(not A) else not B))
assert bool(true(A) else B) == bool(not (not B if true(not A)))

注意右手边的运算顺序(在反转输入断路器之后应用 true)——这确保了实际上对 type(A).__not__ 做出了断言,而不仅仅是关于 type(true(A)).__not__ 的行为。

至少,types.CircuitBreaker 实例会尊重这种逻辑,允许现有的布尔表达式优化(如双重否定消除)继续应用。

任意哨兵对象

与 PEP 505 和 PEP 531 不同,本 PEP 中的提案轻松地处理自定义哨兵对象。

_MISSING = object()

# Using the sentinel to check whether or not an argument was supplied
def my_func(arg=_MISSING):
    arg = make_default() if is_sentinel(arg, _MISSING) # "else arg" implied

断路器表达式中隐式定义的断路器

本 PEP 的一个从未发布的草案探讨了对 isis not 二元运算符进行特殊情况处理的想法,以便在断路表达式上下文中使用时,它们会自动被视为断路器。不幸的是,事实证明,这种方法必然会导致两种非常不希望出现的结果之一:

  1. 这些表达式的返回类型普遍从 bool 更改为 types.CircuitBreaker,可能会产生向后兼容性问题(尤其是在处理专门查找内置布尔值的扩展模块 API 时,使用 PyBool_Check 而不是将提供的 value 通过 PyObject_IsTrue 传递或在其中一个参数解析函数中使用 p(谓词)格式)。
  2. 这些表达式的返回类型变得依赖于上下文,这意味着其他常规重构(例如将比较运算提取到局部变量中)会对一段代码的运行时语义产生重大影响。

这两个可能的结果似乎都没有本 PEP 中的提案所保证,因此它恢复了当前的设计,在当前的设计中,断路器实例必须通过 API 调用显式创建,并且绝不会隐式生成。

实现

PEP 505 一样,实际的实现已被推迟,等待对进行这些更改的想法的原则性兴趣。

…待定…

致谢

感谢 Steven D’Aprano 对本 PEP 初稿的详细批评 [2],它启发了第二稿中的许多更改,以及该讨论线程中所有其他参与者 [3]

参考文献


来源:https://github.com/python/peps/blob/main/peps/pep-0532.rst

最后修改时间:2023-10-11 12:05:51 GMT