python bug with __le__, __ge__?

Is it me or python that is confused with the following code ? I would expect __le__ to be called by a <= ab, not __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

I get the same behavior with python 2.7, 3.2 and pypy 1.9.

What can I do to get __le__ called instead of __ge__ ??

Best answer

The short answer is that they wanted to allow AB to override the behavior from A. Python can’t call AB.__lt__(a, ab), because a may not be a valid self for an AB method, so instead, it calls AB.__gt__(ab, a), which is valid.

The long answer is a bit more complicated.

According to the docs for rich comparison operators:

There are no swapped-argument versions of these methods (to be used when the left argument does not support the operation but the right argument does); rather, __lt__() and __gt__() are each other’s reflection, __le__() and __ge__() are each other’s reflection, and __eq__() and __ne__() are their own reflection.

In other words, x <= y will call y.__ge__(x) in exactly the same cases where x+y would call y.__radd__(x). To compare:

>>> 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

According to the docs for reflected operators:

These methods are called to implement the binary arithmetic operations… with reflected (swapped) operands. These functions are only called if the left operand does not support the corresponding operation and the operands are of different types…

Note: If the right operand’s type is a subclass of the left operand’s type and that subclass provides the reflected method for the operation, this method will be called before the left operand’s non-reflected method. This behavior allows subclasses to override their ancestors’ operations.

So, because XY is a subclass of X, XY.__radd__ gets preference over X.__add__. And, likewise, because AB is a subclass of A, AB.__ge__ gets preference over A.__le__.

This probably should be documented better. To figure it out, you have to ignore the parenthetical “to be used when the left argument does not support the operation but the right argument does”, guess that you need to look up the normal swapped operators (there’s no link, or even mention, here), then ignore the wording that says “These functions are only called if the left operand does not support the corresponding operation”, and see the “Note”, which contradicts what came above… Also notice that the docs explicitly say, “There are no implied relationships among the comparison operators”, only a paragraph before describing the swapped cases, which imply exactly such relationships…

Finally, this case seems odd, because AB, rather than overriding __ge__ itself, just inherited it from B, which knows nothing about A and is unrelated to it. Presumably B didn’t intend to have its subclasses override A‘s behavior. But if B were meant to be used as a mixin for A-derived classes, maybe it would intend exactly such an override. And at any rate, the rule is probably already complicated enough without getting into where each method came from in the MRO. Whatever the reasoning, where the __ge__ comes from is irrelevant; if it’s there on the subclass, it gets called.

For your added final, question, “What can I do to get __le__ called instead of __ge__ ??”… well, you really can’t, any more than you can get X.__add__ called instead of XY.__radd__. Of course you can always implement an AB.__ge__ (or XY.__radd__) that calls A.__le__ (or X.__add__), but it’s presumably easier to just implement AB.__ge__ in such a way that it works with an A as its other argument in the first place. Alternatively, you could remove the inheritance and find some other way to model whatever you were modeling that way. Or you could explicitly call a.__le__(ab) instead of a<=ab. But otherwise, if you designed your classes in a way that takes advantage of the “no implied relationships” to do something weird, you were misled by the docs, and will have to redesign them somehow.