What is the standard way of making a class comparable in Python 3? (For example, by id.)
You said you are trying to do this:
max((f(obj), obj) for obj in obj_list)[1]
You should simply do this:
max(f(obj) for obj in obj_list)
EDIT: Or as gnibbler said: max(obj_list, key=f)
But you told gnibbler you need an object reference to the max object. I think this is simplest:
def max_obj(obj_list, max_fn):
if not obj_list:
return None
obj_max = obj_list[0]
f_max = max_fn(obj)
for obj in obj_list[1:]:
if max_fn(obj) > f_max:
obj_max = obj
return obj_max
obj = max_obj(obj_list)
Of course you might want to let it raise an exception rather than return none if you try to find the max_obj() of an empty list.
Not sure if this is complete, but you'd want to define:
__eq__, __gt__, __ge__, __lt__, __le__
As agf said, I'm missing:
__ne__
For a full set of comparison functions I have used the following mixin, which you could put in say for example a mixin.py in your module.
class ComparableMixin(object):
def _compare(self, other, method):
try:
return method(self._cmpkey(), other._cmpkey())
except (AttributeError, TypeError):
# _cmpkey not implemented, or return different type,
# so I can't compare with "other".
return NotImplemented
def __lt__(self, other):
return self._compare(other, lambda s, o: s < o)
def __le__(self, other):
return self._compare(other, lambda s, o: s <= o)
def __eq__(self, other):
return self._compare(other, lambda s, o: s == o)
def __ge__(self, other):
return self._compare(other, lambda s, o: s >= o)
def __gt__(self, other):
return self._compare(other, lambda s, o: s > o)
def __ne__(self, other):
return self._compare(other, lambda s, o: s != o)
To use the mixin above you need to implement a _cmpkey() method that returns a key of objects that can be compared, similar to the key() function used when sorting. The implementation could look like this:
>>> from .mixin import ComparableMixin
>>> class Orderable(ComparableMixin):
...
... def __init__(self, firstname, lastname):
... self.first = firstname
... self.last = lastname
...
... def _cmpkey(self):
... return (self.last, self.first)
...
... def __repr__(self):
... return "%s %s" % (self.first, self.last)
...
>>> sorted([Orderable('Donald', 'Duck'),
... Orderable('Paul', 'Anka')])
[Paul Anka, Donald Duck]
The reason I use this instead of the total_ordering recipe is this bug. It's fixed in Python 3.4, but often you need to support older Python versions as well.
To make classes comparable, you only need to implement __lt__
and decorate the class with functools.total_ordering. This provides the rest of the comparison operators so you don't have to write any of them yourself.
I just thought of a really hackish way to do it. This is in the same spirit as what you were originally trying to do. It does not require adding any functions to the class object; it works for any class.
max(((f(obj), obj) for obj in obj_list), key=lambda x: x[0])[1]
I really don't like that, so here's something less terse that does the same thing:
def make_pair(f, obj):
return (f(obj), obj)
def gen_pairs(f, obj_list):
return (make_pair(f, obj) for obj in obj_list)
def item0(tup):
return tup[0]
def max_obj(f, obj_list):
pair = max(gen_pairs(f, obj_list), key=item0)
return pair[1]
Or, you could use this one-liner if obj_list
is always an indexable object like a list:
obj_list[max((f(obj), i) for i, obj in enumerate(obj_list))[1]]
This has the advantage that if there are multiple objects such that f(obj)
returns an identical value, you know which one you will get: the one with the highest index, i.e. the latest one in the list. If you wanted the earliest one in the list, you could do that with a key function.