For reference, here after is how I see now the implementations (given in Python for clarity instead of C like the actual CPython implementations) of the comparison operators (==
, !=
, <
, >
, <=
and >=
) and their associated default special comparison methods (__eq__
, __ne__
, __lt__
, __gt__
, __le__
and __ge__
) that would add the missing mathematical features from the theory of binary relations that we have been discussing in this thread with @guido and @brettcannon, which are:
- the union relationships ≤ is the union of < and =, and ≥ is the union of > and =;
- the irreflexivity properties of < and >;
- the reflexivity properties of ≤ and ≥.
Following these implementations is a test suite for which every assertion fails with the current implementations but succeeds with the new implementations. The aim is to show the differences in semantics as well as the possible benefits and drawbacks of the new implementations.
Implementations of the comparison operators
The differences between the current and new implementations are indicated. The name eq
stands for ==
, ne
for !=
, lt
for <
, gt
for >
, le
for <=
and ge
for >=
.
def eq(left, right):
if type(left) != type(right) and isinstance(right, type(left)):
result = right.__eq__(left)
if result is NotImplemented:
result = left.__eq__(right)
else:
result = left.__eq__(right)
if result is NotImplemented:
result = right.__eq__(left)
if result is NotImplemented:
# Current implementation: return left is right
# New implementation:
raise TypeError(
"'==' not supported between instances of "
f"'{type(left).__name__}' and '{type(right).__name__}'"
)
return result
def ne(left, right):
if type(left) != type(right) and isinstance(right, type(left)):
result = right.__ne__(left)
if result is NotImplemented:
result = left.__ne__(right)
else:
result = left.__ne__(right)
if result is NotImplemented:
result = right.__ne__(left)
if result is NotImplemented:
# Current implementation: return left is not right
# New implementation:
raise TypeError(
"'!=' not supported between instances of "
f"'{type(left).__name__}' and '{type(right).__name__}'"
)
return result
def lt(left, right):
if type(left) != type(right) and isinstance(right, type(left)):
result = right.__gt__(left)
if result is NotImplemented:
result = left.__lt__(right)
else:
result = left.__lt__(right)
if result is NotImplemented:
result = right.__gt__(left)
if result is NotImplemented:
raise TypeError(
"'<' not supported between instances of "
f"'{type(left).__name__}' and '{type(right).__name__}'"
)
return result
def gt(left, right):
if type(left) != type(right) and isinstance(right, type(left)):
result = right.__lt__(left)
if result is NotImplemented:
result = left.__gt__(right)
else:
result = left.__gt__(right)
if result is NotImplemented:
result = right.__lt__(left)
if result is NotImplemented:
raise TypeError(
"'>' not supported between instances of "
f"'{type(left).__name__}' and '{type(right).__name__}'"
)
return result
def le(left, right):
if type(left) != type(right) and isinstance(right, type(left)):
result = right.__ge__(left)
if result is NotImplemented:
result = left.__le__(right)
else:
result = left.__le__(right)
if result is NotImplemented:
result = right.__ge__(left)
if result is NotImplemented:
raise TypeError(
"'<=' not supported between instances of "
f"'{type(left).__name__}' and '{type(right).__name__}'"
)
return result
def ge(left, right):
if type(left) != type(right) and isinstance(right, type(left)):
result = right.__le__(left)
if result is NotImplemented:
result = left.__ge__(right)
else:
result = left.__ge__(right)
if result is NotImplemented:
result = right.__le__(left)
if result is NotImplemented:
raise TypeError(
"'>=' not supported between instances of "
f"'{type(left).__name__}' and '{type(right).__name__}'"
)
return result
Implementations of the default special comparison methods
The differences between the current and new implementations are indicated.
def __eq__(self, other):
return self is other or NotImplemented # reflexivity property
def __ne__(self, other):
result = self.__eq__(other)
if result is not NotImplemented:
return not result
return NotImplemented
def __lt__(self, other):
# Current implementation: return NotImplemented
# New implementation:
return self is not other and NotImplemented # irreflexivity property
def __gt__(self, other):
# Current implementation: return NotImplemented
# New implementation:
return self is not other and NotImplemented # irreflexivity property
def __le__(self, other):
# Current implementation: return NotImplemented
# New implementation:
result_1 = self.__eq__(other)
if result_1 is True:
return True # union relationship
result_2 = self.__lt__(other)
if result_2 is True:
return True # union relationship
if result_1 is False and result_2 is False:
return False # union relationship
return NotImplemented
def __ge__(self, other):
# Current implementation: return NotImplemented
# New implementation:
result_1 = self.__eq__(other)
if result_1 is True:
return True # union relationship
result_2 = self.__gt__(other)
if result_2 is True:
return True # union relationship
if result_1 is False and result_2 is False:
return False # union relationship
return NotImplemented
Benefits and drawbacks of the new implementations
For the union relationships:
class X:
def __init__(self, attr):
self.attr = attr
def __eq__(self, other):
if isinstance(other, __class__):
return self.attr == other.attr
return NotImplemented
def __lt__(self, other):
if isinstance(other, __class__):
return self.attr < other.attr
return NotImplemented
def __gt__(self, other):
if isinstance(other, __class__):
return self.attr > other.attr
return NotImplemented
x, y = X(3), X(3)
assert x <= y is True # instead of raising a `TypeError`
assert x >= y is True # instead of raising a `TypeError`
assert x.__le__(y) is True # instead of returning `NotImplemented`
assert x.__ge__(y) is True # instead of returning `NotImplemented`
-
Benefits: no need to override
__le__
and __ge__
anymore.
-
Drawbacks: @guido raised concerns about possible backward compatibility issues.
For the irreflexivity properties:
class X:
pass
x, y = X(), X()
assert x < x is False # instead of raising a `TypeError`
assert x > x is False # instead of raising a `TypeError`
assert x.__lt__(x) is False # instead of returning `NotImplemented`
assert x.__gt__(x) is False # instead of returning `NotImplemented`
try:
x != y
except TypeError:
pass # instead of returning `True`
else:
assert False
-
Benefits:
<
, >
, __lt__
and __gt__
return the only possible result for identical objects, and !=
does not return an arbitrary result for non-identical objects (and consequently the user is fully in control of the behaviour of !=
by overriding __ne__
since !=
raises a TypeError
instead of returning an arbitrary fallback when both __ne__
return NotImplemented
).
-
Drawbacks: [to be determined].
For the reflexivity properties:
class X:
pass
x, y = X(), X()
assert x <= x is True # instead of raising a `TypeError`
assert x >= x is True # instead of raising a `TypeError`
assert x.__le__(x) is True # instead of returning `NotImplemented`
assert x.__ge__(x) is True # instead of returning `NotImplemented`
try:
x == y
except TypeError:
pass # instead of returning `False`
else:
assert False
-
Benefits:
<=
, >=
, __le__
and __ge__
return the only possible result for identical objects, and ==
does not return an arbitrary result for non-identical objects (and consequently the user is fully in control of the behaviour of ==
by overriding __eq__
since ==
raises a TypeError
instead of returning an arbitrary fallback when both __eq__
return NotImplemented
).
-
Drawbacks: [to be determined].