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月5日

目录

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 中提出的丰富的比较链模型

利用新协议,本PEP进一步提议修订条件表达式的定义,以允许 ifelse 分别用作右结合和左结合的通用短路运算符。

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

为了使逻辑反转 (not EXPR) 与上述更改保持一致,本PEP还提议引入一个新的逻辑反转协议(使用方法名 __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的直接继承者,它将其中定义的现有检查协议以及新的 ?then?else 语法运算符替换为新的短路协议以及对条件表达式和 not 运算符的调整。

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__ 的短路器)仅在表达式中涉及的所有短路器都使用一致的“真”定义时,才需要遵守布尔逻辑的完整语义。这将在尊重德摩根定律中进一步讨论。

强制短路行为

短路器的短路行为可以通过在条件表达式中将其作为所有三个操作数来强制执行:

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 类型,它实现适当的短路逻辑,以及 operator 模块中的工厂函数,这些函数对应于 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 模块访问过。

短路运算符和短路器的引入允许该逻辑在 operator 模块中按如下方式捕获:

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 条件以及列表推导式和生成器表达式的过滤子句中使用条件表达式 (if-else) 和独立的短路运算符 (ifelse)。

基本原理

添加新运算符

PEP 335 类似,本 PEP 的早期草案侧重于使现有 andor 运算符的解释不那么僵化,而不是提议新的运算符。然而,这被证明存在一些关键问题:

  • andor 运算符具有长期建立的稳定含义,因此如果它们的含义现在取决于左操作数的类型,读者将不可避免地感到惊讶。即使是新用户也会因25年多来教授的教材都假设这些运算符当前众所周知的语义而对这种变化感到困惑。
  • 包括 CPython 在内的 Python 解释器实现利用了 andor 的现有语义来定义运行时和编译时优化,如果这些操作的语义发生变化,所有这些优化都需要进行审查并可能被废弃。
  • 目前尚不清楚定义协议所需的新方法应采用哪些名称。

提议现有的 if-else 三元运算符的短路二元变体则解决了所有这些问题:

  • andor 的运行时语义保持完全不变。
  • 虽然一元 not 运算符的语义确实发生了变化,但 __not__ 实现所需的不变式意味着布尔上下文中现有的表达式优化将仍然有效。
  • 由于缺少尾随的 else 子句,__else__if 表达式的短路结果。
  • 由于缺少前导 if 子句,__then__else 表达式的短路结果(如果方法名称是 __if__,这种关联会更清晰,但考虑到 if 关键字的其他用途不会调用短路协议,这会产生歧义)。

命名运算符和协议

“短路运算符”、“短路协议”和“短路器”这些名称都受到“短路运算符”这一短语的启发:这是指那些仅在有条件地评估其右操作数的运算符的通用语言设计术语。

电气类比是,Python 中的短路器在表达式触发任何异常之前检测并处理短路,类似于电气系统中的短路器在损坏任何设备或伤害任何人之前检测并处理短路。

Python 层面上的类比是,就像 break 语句可以让你在循环达到自然结束之前终止循环一样,短路表达式可以让你立即终止表达式的评估并产生结果。

使用现有关键字

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

ifelse 在语义上适用于提议的新协议,唯一引入的额外语法歧义是在新运算符与显式 if-else 条件表达式语法结合时产生的。

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

命名协议方法

命名 __else__ 方法很简单,因为重用运算符关键字名称会生成一个既明显又无歧义的特殊方法名称。

命名 __then__ 方法则不那么简单,因为还有另一个可能的选择是使用基于关键字的名称 __if__

__if__ 的问题在于,在许多情况下,if 关键字会紧跟一个表达式出现,但 __if__ 特殊方法却不会被调用。相反,bool() 内置函数及其底层特殊方法 (__bool__, __len__) 会被调用,而 __if__ 没有效果。

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

使二元 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 二元比较运算符的更改定义方式使得基于现有语义的控制流优化仍然有效。

这种方法的一个结果是,本 PEP *本身* 对最终用户没有带来多少直接好处,除了使得在特定形式的条件表达式中可以省略一些常见的 None if 前缀和 else None 后缀。

相反,它主要提供了一个共同的基础,使 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 是一个辅助函数,它调用提议的 operator.is_not_sentinel types.CircuitBreaker 工厂,其中 None 作为哨兵值。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

尊重德摩根定律

andor 类似,二元短路运算符将允许以多种方式编写本质上相同的表达式。这种看似冗余的情况不幸是由于将协议定义为完整的布尔代数所隐含的后果,因为布尔代数遵守一对被称为“德摩根定律”的属性:能够将 andor 操作的结果表示为彼此和适当的 not 操作组合。

对于 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 和 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,可能导致向后兼容性问题(尤其是在使用专门查找内置布尔值并使用 PyBool_Check 而不是通过 PyObject_IsTrue 传递提供的值,或者在参数解析函数中使用 p (predicate) 格式的扩展模块 API 时)。
  2. 这些表达式的返回类型变为 *上下文相关的*,这意味着其他常规重构(例如将比较操作提取到局部变量中)可能会对一段代码的运行时语义产生显著影响。

本 PEP 中的提案似乎无法证明这两种可能的结果都是合理的,因此它又回到了当前的设计,即短路器实例必须通过 API 调用显式创建,并且绝不会隐式生成。

实施

PEP 505一样,实际实现已推迟,等待对这些更改的理念原则上的兴趣。

……待定……

致谢

感谢 Steven D’Aprano 对本 PEP 初始草案的详细批评 [2],它启发了第二份草案中的许多更改,也感谢该讨论串中的所有其他参与者 [3]

参考资料


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

最后修改: 2025-02-01 08:59:27 GMT