Python Rich Comparison

Death to Boilerplate - A Mixin Class

 

 

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