Python Rich Comparison
Death to Boilerplate - A Mixin Class

Python and Rich Comparison
Python allows you to implement comparison for custom objects by operator overloading. You can either implement __cmp__, or all the rich comparison methods.
For example, if you want your custom objects to be only comparable for equality and inequality with other objects, then you can provide the __eq__ and __ne__ methods. But you have to implement both. I think that every time I have implemented __eq__, the implementation of __ne__ has always amounted to nothing more than return not self.__eq__(other). Even if it doesn't work for 100% of the cases it would make a damn fine fallback...
In practise, all of the rich comparison methods can be deduced from just implementing __eq__ and __lt__ and sensible fallbacks.
Interestingly, IronPython has it a little better. You still have to provide both __eq__ and __ne__, but you only need to provide __lt__ or __gt__ as well; and then all comparisons give the right answer.
Take the following class:
def __init__(self, value):
self.value = value
def __eq__(self, other):
if hasattr(other, 'value'):
return self.value == other.value
return self.value == other
def __ne__(self, other):
return not self.__eq__(other)
def __lt__(self, other):
if hasattr(other, 'value'):
return self.value < other.value
return self.value < other
With CPython, it gives the right answer for equality / inequality and less than tests. For the other tests, it gives the wrong answers:
>>> c == 6
True
>>> c != 5
True
>>> c < 7
True
>>> c <= 6
False
>>> c > 7
True
>>>
The answers to the last two comparisons should be True and False. With IronPython (IronPython 1 only I'm afraid), we get the following:
>>> c == 6
True
>>> c != 5
True
>>> c < 7
True
>>> c <= 6
True
>>> c > 7
False
>>>
Under the hood, .NET uses the comparison methods we provide (you can verify this by putting prints in to see which methods are called).
To avoid the boilerplate, here's a RichComparisonMixin class. If you only want equality and inequality, then you only need to provide __eq__ in subclasses. For all the comparison methods, you only need to provide __eq__ and __lt__:
def __eq__(self, other):
raise NotImplementedError("Equality not implemented")
def __lt__(self, other):
raise NotImplementedError("Less than not implemented")
def __ne__(self, other):
return not self.__eq__(other)
def __gt__(self, other):
return not (self.__lt__(other) or self.__eq__(other))
def __le__(self, other):
return self.__eq__(other) or self.__lt__(other)
def __ge__(self, other):
return self.__eq__(other) or self.__gt__(other)
To verify that it works, here are the tests. This includes an example class, RichComparer that subclasses RichComparisonMixin. All the rich comparison methods work, even though it only implements __eq__ and __lt__ :
from richcomparisonmixin import RichComparisonMixin
class RichComparer(RichComparisonMixin):
def __init__(self, value):
self.value = value
def __eq__(self, other):
if not hasattr(other, 'value'):
return self.value == other
return self.value == other.value
def __lt__(self, other):
if not hasattr(other, 'value'):
return self.value < other
return self.value < other.value
class RichComparisonMixinTest(TestCase):
def setUp(self):
TestCase.setUp(self)
self.comp = RichComparer(6)
def testDefaultComparison(self):
self.assertRaises(NotImplementedError,
lambda: RichComparisonMixin() == 3)
self.assertRaises(NotImplementedError,
lambda: RichComparisonMixin() != 3)
self.assertRaises(NotImplementedError,
lambda: RichComparisonMixin() < 3)
self.assertRaises(NotImplementedError,
lambda: RichComparisonMixin() > 3)
self.assertRaises(NotImplementedError,
lambda: RichComparisonMixin() <= 3)
self.assertRaises(NotImplementedError,
lambda: RichComparisonMixin() >= 3)
def testEquality(self):
self.assertTrue(self.comp == 6)
self.assertTrue(self.comp == RichComparer(6))
self.assertFalse(self.comp == 7)
self.assertFalse(self.comp == RichComparer(7))
def testInEquality(self):
self.assertFalse(self.comp != 6)
self.assertFalse(self.comp != RichComparer(6))
self.assertTrue(self.comp != 7)
self.assertTrue(self.comp != RichComparer(7))
def testLessThan(self):
self.assertTrue(self.comp < 7)
self.assertTrue(self.comp < RichComparer(7))
self.assertFalse(self.comp < 5)
self.assertFalse(self.comp < RichComparer(5))
self.assertFalse(self.comp < 6)
self.assertFalse(self.comp < RichComparer(6))
def testGreaterThan(self):
self.assertTrue(self.comp > 5)
self.assertTrue(self.comp > RichComparer(5))
self.assertFalse(self.comp > 7)
self.assertFalse(self.comp > RichComparer(7))
self.assertFalse(self.comp > 6)
self.assertFalse(self.comp > RichComparer(6))
def testLessThanEqual(self):
self.assertTrue(self.comp <= 7)
self.assertTrue(self.comp <= RichComparer(7))
self.assertTrue(self.comp <= 6)
self.assertTrue(self.comp <= RichComparer(6))
self.assertFalse(self.comp <= 5)
self.assertFalse(self.comp <= RichComparer(5))
def testGreaterThanEqual(self):
self.assertTrue(self.comp >= 5)
self.assertTrue(self.comp >= RichComparer(5))
self.assertTrue(self.comp >= 6)
self.assertTrue(self.comp >= RichComparer(6))
self.assertFalse(self.comp >= 7)
self.assertFalse(self.comp >= RichComparer(7))
if __name__ == '__main__':
main()
You can download the mixin class and the tests from the recipebook.
For buying techie books, science fiction, computer hardware or the latest gadgets: visit The Voidspace Amazon Store.
Last edited Fri Nov 27 18:32:35 2009.
Counter...

