Python Rich Comparison

Death to Boilerplate - A Mixin Class

Follow my exploration of living a spiritual life and finding the kingdom at Unpolished Musings.

Some things, just don't compare.

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:

class Comparer(object):
    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 = Comparer(6)
>>> 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 = Comparer(6)
>>> 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__:

class RichComparisonMixin(object):

    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 unittest import main, TestCase
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.

Hosted by Webfaction

Return to Top

Page rendered with rest2web the Site Builder

Last edited Tue Aug 2 00:51:34 2011.

Counter...