Working example
Consider a class C
defining all rich comparison methods based on it’s attribute C.val: int
.
Implementation
class C:
def __new__(cls, val: int):
inst = object.__new__(cls)
inst.val = val
return inst
def __eq__(self, other: object) -> bool:
if isinstance(other, C):
return self.val == other.val
return False
def __lt__(self, other: object) -> bool:
if isinstance(other, C):
return self.val < other.val
return NotImplemented
def __le__(self, other: object) -> bool:
if isinstance(other, C):
return self.val <= other.val
return NotImplemented
def __gt__(self, other: object) -> bool:
if isinstance(other, C):
return self.val > other.val
return NotImplemented
def __ge__(self, other: object) -> bool:
if isinstance(other, C):
return self.val >= other.val
return NotImplemented
Consider a class D
inheriting C
and adding “granularity” to these comparisons through it’s attribute D.subval: int
(such that C(3) < D(3, 1) < d(3, 2) < C(4)
.
Implementation
from functools import _ge_from_lt, _gt_from_lt, _le_from_lt
class D(C):
def __new__(cls, val: int, subval: int):
inst = C.__new__(cls, val)
inst.subval = subval
return inst
def __eq__(self, other: object) -> bool:
if isinstance(other, D):
return C.__eq__(self, other) and self.subval == other.subval
return False
def __lt__(self, other: object) -> bool:
if isinstance(other, D):
return C.__lt__(self, other) or (
C.__eq__(self, other) and self.subval < other.subval
)
if isinstance(other, C):
return C.__lt__(self, other)
return NotImplemented
__le__ = _le_from_lt # Can't use @total_ordering because these methods
__gt__ = _gt_from_lt # are defined in class C, so @total_ordering
__ge__ = _ge_from_lt # would not override them
Now, as the docs state,
If the operands are of different types, and right operand’s type is a direct or indirect subclass of the left operand’s type, the reflected method of the right operand has priority, otherwise the left operand’s method has priority.
And indeed
>>> c = C(3)
>>> d = D(3, 1)
>>> D.__gt__(d, c)
True
>>> C.__lt__(c, d)
False
>>> c < d
True
Even if C.__lt__
if explicitely defined, D.__gt__
has priority because issubclass(D, C)
.
The problem
Now, if we try the same thing with a class J
inheriting builtin class int
:
Implementation
class J(int):
def __new__(cls, val: int, subval: int):
inst = int.__new__(cls, val)
inst.subval = subval
return inst
def __eq__(self, other: object) -> bool:
if isinstance(other, J):
return int.__eq__(self, other) and self.subval == other.subval
return False
def __lt__(self, other: object) -> bool:
if isinstance(other, J):
return int.__lt__(self, other) or int.__eq__(self, other) and self.subval < other.subval
if isinstance(other, int):
return int.__lt__(self, other)
return NotImplemented
__le__ = _le_from_lt
__gt__ = _gt_from_lt
__ge__ = _ge_from_lt
We have
>>> i = int(3)
>>> j = J(3, 1)
>>> J.__gt__(j, i)
True
>>> int.__lt__(i, j)
False
>>> i < j
False
It looks like the prementionned mechanism does not work with builtin classes: even if issubclass(J, int)
, J.__gt__
does not have priority over int.__lt__
and the comparaison results are not those expected.
Is this behavior deliberate? The docs only warn against “virtual subclassing”, but we’re not in that case, are we?
Is there a way to implement this behavior with builtin classes?
Thanks