43

What is the standard way of making a class comparable in Python 3? (For example, by id.)

9
  • 2
    Uh, why would you want to sort by the arbitrarily assigned memory address of your class? Commented Aug 2, 2011 at 4:58
  • 1
    sure you could, but would it make sense to order things by their memory locations? Commented Aug 2, 2011 at 4:58
  • 1
    @Tim: Because I am doing: max((f(obj), obj) for obj in obj_list)[1] to get the obj with the highest value according to f. Python 3 complains that obj is not comparable. I don't really care how it's compared. Commented Aug 2, 2011 at 4:59
  • 5
    Surely you mean to say max(obj_list, key=f) instead of that monstrosity Commented Aug 2, 2011 at 5:07
  • 1
    @gnibbler: Thanks!! But what if I want both the obj as well as f(obj)? Commented Aug 2, 2011 at 5:08

6 Answers 6

55

To make classes comparable, you only need to implement __lt__ and decorate the class with functools.total_ordering. You should also provide an __eq__ method if possible. This provides the rest of the comparison operators so you don't have to write any of them yourself.

Sign up to request clarification or add additional context in comments.

9 Comments

+1 Just want to mention for anyone else searching this decorator is available as functools.total_ordering.
@Neil G : + everyone else: Also be aware that that decorator has bugs, and shouldn't be used if you ever expect anyone to compare your class with any other class bugs.python.org/issue10042 My preferred method is a mixin: python3porting.com/preparing.html#comparatively-tricky
@Lennart: Please add an answer. I did notice that mixin. Why wasn't it added to the Python standard library like the decorator?
This has been fixed in 3.4, and it is probably the best answer.
@EntropicThunder Added to answer.
|
15

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.

4 Comments

It's worth pointing out to anyone finding this answer that the mixin module is a hypothetical module one could define — it's not in the Python standard library.
The bug has now been fixed. Would you mind updating your answer or adding a new one?
Thank you! So, in Python >3.4, you would recommend applying the decorator functools.total_ordering?
I haven't looked at the final solution they put into 3.4, so I can't tell for sure, but it's probably fine.
1

Since Python 3.7 you can use the @dataclass:

from dataclasses import dataclass @dataclass(order=True) class MyObject(object): def __init__(self, *, name=None): self.name = name 

Comments

0

Not sure if this is complete, but you'd want to define:

__eq__, __gt__, __ge__, __lt__, __le__ 

As agf said, I'm missing:

__ne__ 

5 Comments

Isn't there a way to have most of these automatically defined with a mixin?
@Neil: And how would the mixin define them?
See the decorator recipe in my answer. Also, the other comparison is __ne__, not equal.
@Ignacio: Perhaps if one is defined, it could use it to define the rest?
never used mixins, sorry can't help there. I'm still a little unsure of myself with Python3 as well, mostly continuing using Python2.
0

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.

4 Comments

The first version breaks ties with the object itself.
Yes, I didn't quite grok what you were doing when I wrote the first version. I think I have it now.
Did you really need to post two separate answers? They're both doing the same thing, and both are way less clear than the original max((f(obj), obj) for obj in obj_list)
Do you consider it some sort of breach of etiquette to post two completely different answers as two separate answers? And do you really think they are doing the same thing? One calls max() with a key, the other is a for loop; how are they doing the same thing? And I am not sure I agree my solutions are less clear. By the way, you left off the [1] from his answer, which was tricky enough it fooled me the first time I read it.
0

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.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.