为什么 Python 不提供 __le__ 和 __ge__ 的默认实现?

Mag*_*ero -3 python relationship partial-ordering comparison-operators

比较关系(=、?、<、>、? 和 ?)之间的以下数学关系始终有效,因此在 Python 中默认实现(除了 2 个联合关系,这似乎是任意的,这也是本文的原因):

  • 2互补关系:“=和?互为互补”;
  • 6个关系*:“=是自身的逆”,“?是自身的逆”,“<和>是彼此的逆”,“?和?是彼此的逆”;
  • 2联合关系:“? 是联合 < 和 =",而“? 是 > 和 =" 的联合。

以下比较关系之间的关系仅对总订单有效,因此在Python中默认不实现(但用户可以functools.total_ordering通过Python标准库提供的类装饰器方便地实现它们):

  • 4种互补关系:“<和?互为补”,">和?互为补”。

为什么 Python 只缺少上面的 2 个联合关系(“?是联合 < 和 =" 和“?是 > 和 =" 的联合)?

它应该提供的默认实现__le__来讲__lt____eq__,和的默认实现__ge__来讲__gt____eq__(性能,但可能是在C,像,像这些__ne__):

def __le__(self, other):
    result_1 = self.__lt__(other)
    result_2 = self.__eq__(other)
    if result_1 is not NotImplemented and result_2 is not NotImplemented:
        return result_1 or result_2
    return NotImplemented

def __ge__(self, other):
    result_1 = self.__gt__(other)
    result_2 = self.__eq__(other)
    if result_1 is not NotImplemented and result_2 is not NotImplemented:
        return result_1 or result_2
    return NotImplemented
Run Code Online (Sandbox Code Playgroud)

这 2 个联合关系始终有效,因此这些默认实现将使用户不​​必一直提供它们(如此)。

这是Python 文档的段落,其中明确指出当前默认情况下未实现 2 个联合关系(粗体强调我的):

默认情况下,__ne__()__eq__()结果委托给并反转结果,除非它是NotImplemented。比较运算符之间没有其他隐含的关系,例如,的真值(x<y or x==y)并不意味着x<=y


* Converse 关系是通过NotImplemented协议在 Python 中实现的。

dec*_*eze 5

为什么做出这个决定只有原作者知道,但根据手册中的这些提示可以推断出原因:

要从单个根操作自动生成排序操作,请参阅functools.total_ordering()

虽然这个装饰器可以很容易地创建表现良好的完全有序类型,但它确实以执行速度较慢和派生比较方法的堆栈跟踪更复杂为代价。如果性能基准测试表明这是给定应用程序的瓶颈,那么实现所有六种丰富的比较方法可能会提供简单的速度提升。

将此与 Python 的口头禅相结合显式优于隐式,以下推理应该是令人满意的:

派生__ne____eq__实际上是免费的,它只是操作not o.__eq__(other),即反转布尔值。

但是,__le____lt__and 的并集派生__eq__意味着需要调用这两种方法,如果完成的比较足够复杂,这可能会对性能造成很大的影响,尤其是与优化的单个__le__实现相比。Python 允许您通过使用total_ordering装饰器显式地选择使用这种性能上的便利,但它不会隐式地将其强加给您。

如果您尝试进行未实现的比较而不是隐式派生的比较,而您没有实现并且可能会产生细微的错误,那么您也可以争论显式错误,这取决于您打算对自定义类做什么。Python 不会在这里为您做任何猜测,而是由您来明确地实现您想要的比较,或者再次明确地选择加入派生比较。

  • 我只能重复一遍:从“__eq__”和“__lt__”派生“__le__”可能会产生性能问题,或者尝试从“not __gt__”派生“__le__”可能会因“NaN”等类型而失败,未实现的比较*不会*引发错误可以说是令人惊讶的行为...... **综合考虑所有这些不同的原因,Python 设计者选择不实现默认的派生比较。 ** 不要关注*一个* 参数,从整体上考虑它们。 (3认同)

Mis*_*agi 5

TLDR:比较运算符不需要返回bool。这意味着结果可能不会严格遵守“a <= ba < b or a == b”或类似的关系。最重要的是,布尔值or可能无法保留其语义。

自动生成特殊方法可能会默默地导致错误的行为,类似于自动__bool__通常不适用的方式。(此示例还将<=etc. 视为不仅仅是bool.)


一个例子是通过比较运算符表达时间点。例如,usim模拟框架(免责声明:我维护这个包)定义了可以检查等待的时间点。我们可以使用比较来描述某个时间点“在或之后”:

  • time > 2000 2000年后。
  • time == 2000 在 2000 年。
  • time >= 2000 2000 年或之后。

(同样适用于<==,但限制更难解释。)

值得注意的是,每个表达式都有两个特征:现在是否满足( bool(time >= 2000)) 和何时满足( await (time >= 2000))。第一个显然可以针对每种情况进行评估。但是,第二个不能。

等待==>=可以通过等待/睡眠直到一个确切的时间点来完成。然而,等待>需要等待一个时间点加上一些无限小的延迟。后者无法准确表达,因为当代数字类型没有通用的无限小但非零的数字。

因此,==和的结果>=从根本上不同于>。派生>=为“ > or ==”是错误的。因此,usim.time定义==>=但不是>为了避免错误。自动定义比较运算符会阻止这种情况,或者错误地定义运算符。

  • @Maggyero 这种推理正是我问你认为可接受的答案的原因。对于“每种”情况,人们可能会添加一些“更多”脚手架,以便为当前默认设置提供逃生舱口。 (2认同)
  • @Maggyero 请重新考虑你的推理思路。到目前为止,为什么 Python 的运算符不可能偏离联合关系的唯一论据是联合关系本身,以各种方式表述。事实上,Python 的运算符不需要遵守并集关系,这使得任何要求与并集关系保持一致的要求都无效。 (2认同)
  • @Maggyero我认为自动生成特殊方法是一个非常糟糕的设计。我特别链接了一个更简单的特殊方法的示例,该方法会自动出现,因此很难规避。该生态系统充斥着无法正确处理此问题的工具,尽管它是最著名的第三方库之一的一部分。最重要的是,绝对没有理由将这种“可证明错误的行为”硬连接到 Python 中,因为它可以根据具体情况通过装饰器或继承根据需要轻松应用。 (2认同)

Mar*_*ers 5

你的问题是基于一些不正确的假设。你开始你的问题:

比较之间的关系下面的数学关系(=?<>??)总是有效的,因此在默认情况下用Python实现(除2个联盟关系,这似乎是任意的,是这个职位的原因)。

目前因为没有默认实现<>两种。没有默认__lt____gt__实现,所以不能成为一个默认的实现__le____ge__两种。*

这包含在值比较下的表达式参考文档中:

相等比较(==!=)的默认行为基于对象的身份。因此,相同身份的实例的相等比较导致相等,不同身份的实例的相等比较导致不平等。这种默认行为的动机是希望所有对象都应该是自反的(即x is y隐含x == y)。

默认顺序的比较(<><=,和>=)不设置; 一次尝试引发TypeError。这种默认行为的动机是缺乏与平等类似的不变量。

默认相等比较的行为,即具有不同身份的实例总是不相等的,可能与需要具有对象值和基于值的相等的合理定义的类型形成对比。这些类型需要自定义它们的比较行为,事实上,许多内置类型已经这样做了。

不提供默认行为的动机包含在文档中。请注意,这些比较是在每个对象的之间进行的,这是一个抽象概念。从同一文档部分,在开始时:

对象的值在 Python 中是一个相当抽象的概念:例如,对象的值没有规范的访问方法。此外,不要求对象的值应以特定方式构造,例如由其所有数据属性组成。比较运算符实现了对象值是什么的特定概念。人们可以将它们视为通过比较实现间接定义对象的值。

因此,比较是在对象价值的一种概念之间进行的。但是这个概念究竟什么,取决于开发人员来实现。Python不会对对象的值做任何假设。这包括假设对象值中存在任何固有的排序。

唯一的原因==是实现在所有的,是因为当x is y为真,则xy是完全相同的对象,所以价值x和价值y是完全一样的事情,因此必须相等。Python 在很多不同的地方都依赖于相等性测试(例如针对列表的包含测试),因此没有默认的相等性概念会使 Python 中的很多事情变得更加困难。!=是 的直接逆==;如果==操作数的值相同时!=为真,则仅在==为假时为真。

你不能说同样的<<==>>没有从开发者的帮助,因为他们需要了解如何的对象需要一个值的抽象的概念进行比较,以其他类似的价值更多的信息。在这里,x <= y不仅仅是 的倒数x > y,因为没有关于xor的值的任何信息y,以及它与==or!=<或任何其他值比较的关系。

您还声明:

这 2 个联合关系始终有效,因此这些默认实现将使用户不​​必一直提供它们

2 联合关系并不总是有效的。可能是>and<运算符实现没有进行比较,并且实现可以自由返回除Trueor之外的结果False。从关于__lt__等方法文档中

但是,这些方法可以返回任何值,因此如果在布尔上下文中使用比较运算符(例如,在 if 语句的条件中),Python 将对该值调用 bool() 以确定结果是 true 还是 false .

如果一个实现决定给两个对象之间的><一个完全不同的含义,开发人员不应该留下错误的默认实现__le____ge__假设实现 for__lt____gt__返回布尔值,因此将调用bool()它们的返回值。这可能是不可取的,开发者应该可以随意重载的意思__bool__呢!

对此的规范示例是 Numpy 库,它是实现这些丰富的比较挂钩的主要驱动程序。Numpy 数组不会为比较操作返回布尔值。相反,它们广播两个数组中所有包含的值之间的操作以生成一个新数组,因此array_a < array_b为来自array_a和 的每个配对值生成一个新的布尔值数组array_b。数组不是布尔值,您的默认实现会因bool(array)引发异常而中断。虽然在 Numpy 的情况下,它们也实现__le____ge__广播比较,但 Python 不能要求所有类型为这些钩子提供实现,只是在不需要时禁用它们。

您似乎将数学关系与 Python 对其中一些关系的使用混为一谈。数学关系适用于某些类别的值(主要是数字)。它们不适用于其他领域,由每种类型实现来决定是否遵守这些数学关系。

最后,之间的互补关系<>=之间,以及><=*只适用于总订单二元关系,如在规定上维基百科文章的部分二元关系

例如,=and?是彼此的补码,就像?and ??and ?、and??and 一样,对于总订单,还有<and?>and ?

Python 不能假设所有类型的实现都希望在它们的值之间创建全序关系。

set例如,标准库类型支持集合之间的全序,set_a < set_bset_a是较大set_b. 这意味着可以有set_c是 的子集,set_bset_c不一定是 的子集或超集set_a。设置的比较也没有连通性,set_a <= set_b并且set_b <= set_a可以两者是假的,在同一时间,当这两组具有不存在于其他元素。


*注:object.__lt__object.__gt__object.__le__object.__ge__方法确实有一个默认的实现,但只能返回NotImplemented无条件。他们的存在只是为了简化的实施<><=>=运营商,这对于a [operator] b需要测试a.__[hook]__(b),然后再尝试b.__[converse hook]__(a)如果第一个回报NotImplemented。如果没有默认实现,那么代码还需要首先检查钩子方法是否存在。尽管如此,使用<or><=or>=在确实提供自己实现的对象上会导致TypeError。难道 将这些视为默认实现,它们不进行任何值比较。