__lt__ instead of __cmp__
Yep, it's easy to implement everything in terms of e.g. __lt__
with a mixin class (or a metaclass, or a class decorator if your taste runs that way).
For example:
class ComparableMixin:
def __eq__(self, other):
return not self<other and not other<self
def __ne__(self, other):
return self<other or other<self
def __gt__(self, other):
return other<self
def __ge__(self, other):
return not self<other
def __le__(self, other):
return not other<self
Now your class can define just __lt__
and multiply inherit from ComparableMixin (after whatever other bases it needs, if any). A class decorator would be quite similar, just inserting similar functions as attributes of the new class it's decorating (the result might be microscopically faster at runtime, at equally minute cost in terms of memory).
Of course, if your class has some particularly fast way to implement (e.g.) __eq__
and __ne__
, it should define them directly so the mixin's versions are not use (for example, that is the case for dict
) -- in fact __ne__
might well be defined to facilitate that as:
def __ne__(self, other):
return not self == other
but in the code above I wanted to keep the pleasing symmetry of only using <
;-).
As to why __cmp__
had to go, since we did have __lt__
and friends, why keep another, different way to do exactly the same thing around? It's just so much dead-weight in every Python runtime (Classic, Jython, IronPython, PyPy, ...). The code that definitely won't have bugs is the code that isn't there -- whence Python's principle that there ought to be ideally one obvious way to perform a task (C has the same principle in the "Spirit of C" section of the ISO standard, btw).
This doesn't mean we go out of our way to prohibit things (e.g., near-equivalence between mixins and class decorators for some uses), but it definitely does mean that we don't like to carry around code in the compilers and/or runtimes that redundantly exists just to support multiple equivalent approaches to perform exactly the same task.
Further edit: there's actually an even better way to provide comparison AND hashing for many classes, including that in the question -- a __key__
method, as I mentioned on my comment to the question. Since I never got around to writing the PEP for it, you must currently implement it with a Mixin (&c) if you like it:
class KeyedMixin:
def __lt__(self, other):
return self.__key__() < other.__key__()
# and so on for other comparators, as above, plus:
def __hash__(self):
return hash(self.__key__())
It's a very common case for an instance's comparisons with other instances to boil down to comparing a tuple for each with a few fields -- and then, hashing should be implemented on exactly the same basis. The __key__
special method addresses that need directly.
To simplify this case there's a class decorator in Python 2.7+/3.2+, functools.total_ordering, that can be used to implement what Alex suggests. Example from the docs:
@total_ordering
class Student:
def __eq__(self, other):
return ((self.lastname.lower(), self.firstname.lower()) ==
(other.lastname.lower(), other.firstname.lower()))
def __lt__(self, other):
return ((self.lastname.lower(), self.firstname.lower()) <
(other.lastname.lower(), other.firstname.lower()))