【问题标题】:python bug with __le__, __ge__?带有 __le__、__ge__ 的 python 错误?
【发布时间】:2025-12-14 04:15:01
【问题描述】:

是我还是 python 对下面的代码感到困惑?我希望__le__a <= ab 调用,而不是__ge__

#!/usr/bin/env python2

class B(object):
    def __ge__(self, other):
        print("__ge__ unexpectedly called")

class A(object):
    def __le__(self, other):
        print("__le__ called")

class AB(A, B):
    pass

a = A()
ab = AB()

a <= ab # --> __ge__ unexpectedly called
ab <= a # --> __le__ called

我在 python 2.7、3.2 和 pypy 1.9 中得到了相同的行为。

我该怎么做才能调用__le__ 而不是__ge__ ??

【问题讨论】:

  • 当你可以使用a = A()ab = AB() 来获得相同的对象时,为什么你会使用make 函数来处理所有复杂的事情? (我知道为什么它在你的实际程序中可能有用,但它根本不会影响这个例子,它可能会甩掉一些想要调查并希望回答的人。)
  • 确实,您提出的简化版本也具有相同的行为。现已更正。

标签: python


【解决方案1】:

简短的回答是他们希望允许AB 覆盖A 的行为。 Python 无法调用AB.__lt__(a, ab),因为a 对于AB 方法可能不是有效的self,因此它调用了有效的AB.__gt__(ab, a)

长答案有点复杂。

根据rich comparison operators 的文档:

这些方法没有交换参数版本(当左参数不支持操作但右参数支持时使用);而__lt__()__gt__()是彼此的反射,__le__()__ge__()是彼此的反射,__eq__()__ne__()是自己的反射。

换句话说,x &lt;= y 将调用y.__ge__(x),与x+y 调用y.__radd__(x) 的情况完全相同。比较:

>>> class X(object):
...     def __add__(self, other):
...         print('X.add')
>>> class Y(object):
...     def __radd__(self, other):
...         print('Y.radd')
>>> class XY(X, Y):
...     pass
>>> x, xy = X(), XY()
>>> x + xy
Y.radd

根据reflected operators 的文档:

调用这些方法来实现二进制算术运算……使用反射(交换)操作数。这些函数只有在左操作数不支持相应操作并且操作数属于不同类型时才会被调用……

注意:如果右操作数的类型是左操作数类型的子类,并且该子类为操作提供了反射方法,则该方法将在左操作数的非反射方法之前调用。这种行为允许子类覆盖其祖先的操作。

所以,因为XYX 的子类,所以XY.__radd__ 优先于X.__add__。同样,因为ABA 的子类,AB.__ge__ 优先于A.__le__

这可能应该更好地记录下来。要弄清楚,您必须忽略括号“当左参数不支持操作但右参数支持时使用”,猜测您需要查找正常的交换运算符(没有链接,甚至提到, 这里),然后忽略“仅当左操作数不支持相应操作时才调用这些函数”的措辞,并查看与上面的内容相矛盾的“注释”……还要注意文档明确说,“比较运算符之间没有隐含关系”,只有在描述交换案例之前的一段,这暗示了这种关系……

最后,这种情况看起来很奇怪,因为AB 并没有覆盖__ge__ 本身,而是从B 继承而来,BA 一无所知,并且与它无关。大概B 不打算让它的子类覆盖A 的行为。但是,如果 B 打算用作 A 派生类的 mixin,那么它可能 打算进行这样的覆盖。无论如何,该规则可能已经足够复杂,而无需深入探讨每种方法在 MRO 中的来源。无论是什么原因,__ge__ 的来源都无关紧要;如果它在子类中,就会被调用。

对于您添加的最后一个问题,“我该怎么做才能让 __le__ 调用而不是 __ge__ ??”......好吧,你真的不能,就像你可以调用 X.__add__ 而不是XY.__radd__。当然,您始终可以实现一个调用A.__le__(或X.__add__)的AB.__ge__(或XY.__radd__),但可能更容易实现AB.__ge__,使其与A一起工作首先作为它的另一个论点。或者,您可以删除继承并找到其他方法来建模您以这种方式建模的任何内容。或者您可以显式调用a.__le__(ab) 而不是a&lt;=ab。但除此之外,如果您以一种利用“无隐含关系”的方式设计类来做一些奇怪的事情,那么您就会被文档误导,并且必须以某种方式重新设计它们。

【讨论】:

  • 因为“右操作数的类型是左操作数类型的子类,并且该子类提供了反射方法”,“允许子类覆盖其祖先的操作”。 (如果你问为什么它很重要,因为 ABB 继承 __ge__ 而不是直接定义它,那么规则没有考虑到这一点。可能是因为事情已经足够复杂而没有添加它混合。)
  • @abarnert 在他的回答 (+1, BTW) 中提到,这是为了赋予更多派生类优先级,以便允许它覆盖行为。
  • @billybob:不,它不会,它会调用ab.__ge__(a),正如你所期望的那样;没有交换。 (另外,a&lt;=ab 没有调用不正确的方法,而是调用了它应该调用的方法。只是文档令人困惑,所以你不知道它应该以这种方式工作。)如果你的意思是 a&gt;=ab而不是ab&gt;=a,实际上是调用ab.__le__(a),在这种情况下恰好是从A继承的,所以和原来的情况一样,有点伪装。
  • 感谢您的回答。我现在有一个解决方法:创建 A 的另一个(空)子类,称为 AA,并使用 aa
  • @billybob:对于新版本:嗯,差不多。对于a&lt;=ab,Python 认为ab.__ge__(a) 优于a.__le__(ab),因为type(ab)type(a) 的子类。但是对于ab&gt;=a,这是因为type(a) 不是type(ab) 的子类。 (如果使用完全不相关的类型,您可以看到区别:a&lt;=b 使用 a.__le__(b)b&gt;=a 使用 b.__ge__(a),因为在这两种情况下,正确的操作数都不是子类实例,因此没有交换。
最近更新 更多