Boilerplate in rich comparison methods

S

Steven D'Aprano

I'm writing a class that implements rich comparisons, and I find myself
writing a lot of very similar code. If the calculation is short and
simple, I do something like this:


class Parrot:
def __eq__(self, other):
return self.plumage() == other.plumage()
def __ne__(self, other):
return self.plumage() != other.plumage()
def __lt__(self, other):
return self.plumage() < other.plumage()
def __gt__(self, other):
return self.plumage() > other.plumage()
def __le__(self, other):
return self.plumage() <= other.plumage()
def __ge__(self, other):
return self.plumage() >= other.plumage()

If the comparison requires a lot of work, I'll do something like this:

class Aardvark:
def __le__(self, other):
return lots_of_work(self, other)
def __gt__(self, other):
return not self <= other
# etc.

But I can't help feeling that there is a better way. What do others do?
 
P

Paul Rubin

Steven D'Aprano said:
class Parrot:
def __eq__(self, other):
return self.plumage() == other.plumage()
def __ne__(self, other):
return self.plumage() != other.plumage()
def __lt__(self, other):
return self.plumage() < other.plumage()
def __gt__(self, other):
return self.plumage() > other.plumage()
def __le__(self, other):
return self.plumage() <= other.plumage()
def __ge__(self, other):
return self.plumage() >= other.plumage()

If it's that uniform I think you can just use __cmp__:

class Parrot:
def __cmp__(self, other):
return cmp(self.plumage(), other.plumage())

Did I miss something? The idea of rich comparison is that the
different relations aren't so similar to each other, e.g. some kind of
partial ordering.
If the comparison requires a lot of work, I'll do something like this:

class Aardvark:
def __le__(self, other):
return lots_of_work(self, other)
def __gt__(self, other):
return not self <= other
# etc.

But I can't help feeling that there is a better way. What do others do?

Same as above.
 
G

George Sakkis

Steven said:
I'm writing a class that implements rich comparisons, and I find myself
writing a lot of very similar code. If the calculation is short and
simple, I do something like this:


class Parrot:
def __eq__(self, other):
return self.plumage() == other.plumage()
def __ne__(self, other):
return self.plumage() != other.plumage()
def __lt__(self, other):
return self.plumage() < other.plumage()
def __gt__(self, other):
return self.plumage() > other.plumage()
def __le__(self, other):
return self.plumage() <= other.plumage()
def __ge__(self, other):
return self.plumage() >= other.plumage()

If the comparison requires a lot of work, I'll do something like this:

class Aardvark:
def __le__(self, other):
return lots_of_work(self, other)
def __gt__(self, other):
return not self <= other
# etc.

But I can't help feeling that there is a better way. What do others do?

Once upon a time I had written a metaclass to generate the boilerate,
filling in the gaps. It doesn't impose any constraints on which
comparisons you must implement, e.g you may implement __le__ and
__eq__, or __gt__ and __ne__, etc. Season to taste.

http://rafb.net/p/mpvsIQ37.nln.html

George
 
P

Paul McGuire

Once upon a time I had written a metaclass to generate the boilerate,
filling in the gaps. It doesn't impose any constraints on which
comparisons you must implement, e.g you may implement __le__ and
__eq__, or __gt__ and __ne__, etc. Season to taste.

http://rafb.net/p/mpvsIQ37.nln.html

George

Just a side note on writing these comparison operators. I remember when
learning Java that this was really the first time I spent so much time
reading about testing-for-identity vs. testing-for-equality. The Java
conventional practice at the time was to begin each test-for-equality method
by testing to see if an object were being compared against itself, and if
so, cut to the chase and return True (and the converse for an inequality
comparison). The idea behind this was that there were ostensibly many times
in code where an object was being compared against itself (not so much in an
explicit "if x==x" but in implicit tests such as list searching and
filtering), and this upfront test-for-identity, being very fast, could
short-circuit an otherwise needless comparison.

In Python, this would look like:

class Parrot:
def __eq__(self, other):
return self is other or self.plumage() == other.plumage()
def __ne__(self, other):
return self is not other and self.plumage() != other.plumage()
def __lt__(self, other):
return self is not other and self.plumage() < other.plumage()
def __gt__(self, other):
return self is not other and self.plumage() > other.plumage()
def __le__(self, other):
return self is not other and self.plumage() <= other.plumage()
def __ge__(self, other):
return self is not other and self.plumage() >= other.plumage()

and George's metaclass would have similar changes.

On the other hand, I haven't seen this idiom in any Python code that I've
read, and I wonder if this was just a coding fad of the time.

Still, in cases such as Steven's Aardark class, it might be worth bypassing
something that calls lots_of_work if you tested first to see if self is not
other.

-- Paul
 
D

Duncan Booth

Paul McGuire said:
In Python, this would look like:

class Parrot:
def __eq__(self, other):
return self is other or self.plumage() == other.plumage()
def __ne__(self, other):
return self is not other and self.plumage() != other.plumage()
def __lt__(self, other):
return self is not other and self.plumage() < other.plumage()
def __gt__(self, other):
return self is not other and self.plumage() > other.plumage()
def __le__(self, other):
return self is not other and self.plumage() <= other.plumage()
def __ge__(self, other):
return self is not other and self.plumage() >= other.plumage()

and George's metaclass would have similar changes.

On the other hand, I haven't seen this idiom in any Python code that
I've read, and I wonder if this was just a coding fad of the time.

It is a perfectly reasonable short-cut for those types where you know an
object is equal to itself, but that isn't always the case. e.g. floating
point NaN values are not equal to themselves, and a list of numbers might
contain a NaN which would mean the list wouldn't be equal to itself.

Also note that for the __le__, __ge__ cases you got the shortcut test the
wrong way round.
 
D

Dan Bishop

I'm writing a class that implements rich comparisons, and I find myself
writing a lot of very similar code. If the calculation is short and
simple, I do something like this:

class Parrot:
def __eq__(self, other):
return self.plumage() == other.plumage()
def __ne__(self, other):
return self.plumage() != other.plumage()
def __lt__(self, other):
return self.plumage() < other.plumage()
def __gt__(self, other):
return self.plumage() > other.plumage()
def __le__(self, other):
return self.plumage() <= other.plumage()
def __ge__(self, other):
return self.plumage() >= other.plumage()

If the comparison requires a lot of work, I'll do something like this:

class Aardvark:
def __le__(self, other):
return lots_of_work(self, other)
def __gt__(self, other):
return not self <= other
# etc.

But I can't help feeling that there is a better way. What do others do?

Typically, I write only two kinds of classes that use comparion
operators: (1) ones that can get by with __cmp__ and (2) ones that
define __eq__ and __ne__ without any of the other four.

But for your case, I'd say you're doing it the right way. If you
define a lot of classes like Parrot, you might want to try moving the
six operators to a common base class:

class Comparable:
"""
Abstract base class for classes using rich comparisons.
Objects are compared using their cmp_key() method.
"""
def __eq__(self, other):
return (self is other) or (self.cmp_key() == other.cmp_key())
def __ne__(self, other):
return (self is not other) and (self.cmp_key() !=
other.cmp_key())
def __lt__(self, other):
return self.cmp_key() < other.cmp_key()
def __le__(self, other):
return self.cmp_key() <= other.cmp_key()
def __gt__(self, other):
return self.cmp_key() > other.cmp_key()
def __ge__(self, other):
return self.cmp_key() >= other.cmp_key()
def cmp_key(self):
"""Overriden by derived classes to define a comparison key."""
raise NotImplementedError()

class Parrot(Comparable):
def cmp_key(self):
return self.plumage()
# ...
 
S

Steven D'Aprano

Just a side note on writing these comparison operators. I remember when
learning Java that this was really the first time I spent so much time
reading about testing-for-identity vs. testing-for-equality. The Java
conventional practice at the time was to begin each test-for-equality method
by testing to see if an object were being compared against itself, and if
so, cut to the chase and return True (and the converse for an inequality
comparison). The idea behind this was that there were ostensibly many times
in code where an object was being compared against itself (not so much in an
explicit "if x==x" but in implicit tests such as list searching and
filtering), and this upfront test-for-identity, being very fast, could
short-circuit an otherwise needless comparison.

In Python, this would look like:

class Parrot:
def __eq__(self, other):
return self is other or self.plumage() == other.plumage()

[snip]

Surely this is only worth doing if the comparison is expensive?
Testing beats intuition, so let's find out...

class Compare:
def __init__(self, x):
self.x = x
def __eq__(self, other):
return self.x == other.x

class CompareWithIdentity:
def __init__(self, x):
self.x = x
def __eq__(self, other):
return self is other or self.x == other.x

Here's the timing results without the identity test:
import timeit
x = Compare(1); y = Compare(1)
timeit.Timer("x = x", "from __main__ import x,y").repeat() [0.20771503448486328, 0.16396403312683105, 0.16507196426391602]
timeit.Timer("x = y", "from __main__ import x,y").repeat()
[0.20918107032775879, 0.16187810897827148, 0.16351795196533203]

And with the identity test:
x = CompareWithIdentity(1); y = CompareWithIdentity(1)
timeit.Timer("x = x", "from __main__ import x,y").repeat() [0.20761799812316895, 0.16907095909118652, 0.16420602798461914]
timeit.Timer("x = y", "from __main__ import x,y").repeat()
[0.2090909481048584, 0.1968839168548584, 0.16479206085205078]

Anyone want to argue that this is a worthwhile optimization? :)
On the other hand, I haven't seen this idiom in any Python code that I've
read, and I wonder if this was just a coding fad of the time.

Still, in cases such as Steven's Aardark class, it might be worth
bypassing something that calls lots_of_work if you tested first to see
if self is not other.

The comparison itself would have to be quite expensive to make it worth
the extra code.
 
S

Steven D'Aprano

[snip more boilerplate code]
If it's that uniform I think you can just use __cmp__:

Good point -- I had somehow picked up the mistaken idea that __cmp__ was
depreciated in favour of rich comparisons.
 
N

Neil Cerutti

On Sat, 13 Jan 2007 10:04:17 -0600, Paul McGuire wrote:
[snip]

Surely this is only worth doing if the comparison is expensive?
Testing beats intuition, so let's find out...

class Compare:
def __init__(self, x):
self.x = x
def __eq__(self, other):
return self.x == other.x

class CompareWithIdentity:
def __init__(self, x):
self.x = x
def __eq__(self, other):
return self is other or self.x == other.x

Here's the timing results without the identity test:
import timeit
x = Compare(1); y = Compare(1)
timeit.Timer("x = x", "from __main__ import x,y").repeat() [0.20771503448486328, 0.16396403312683105, 0.16507196426391602]
timeit.Timer("x = y", "from __main__ import x,y").repeat()
[0.20918107032775879, 0.16187810897827148, 0.16351795196533203]

And with the identity test:
x = CompareWithIdentity(1); y = CompareWithIdentity(1)
timeit.Timer("x = x", "from __main__ import x,y").repeat() [0.20761799812316895, 0.16907095909118652, 0.16420602798461914]
timeit.Timer("x = y", "from __main__ import x,y").repeat()
[0.2090909481048584, 0.1968839168548584, 0.16479206085205078]

Anyone want to argue that this is a worthwhile optimization? :)

Perhaps. But first test it with "==".
 
S

Steven D'Aprano

Perhaps. But first test it with "==".

Oh the ignominy! That's what happens when I run code at 6am :(
x = CompareWithIdentity(1); y = CompareWithIdentity(1)
timeit.Timer("x == y", "from __main__ import x,y").repeat() [2.2971229553222656, 2.2821698188781738, 2.2767620086669922]
timeit.Timer("x == x", "from __main__ import x,y").repeat()
[1.6935880184173584, 1.6783449649810791, 1.6613109111785889]
[2.1717329025268555, 2.1361908912658691, 2.1338419914245605]

So for this simple case, testing for identity is a factor of 1.3 faster
when the objects are identical, and a factor of 1.1 slower if they aren't.
That suggests that if about 33% of your comparisons match by identity,
you'll break-even; any less than that, and the optimization is actually a
pessimation.
 
G

Gabriel Genellina

"Steven D'Aprano" <[email protected]> escribió en el
mensaje
Good point -- I had somehow picked up the mistaken idea that __cmp__ was
depreciated in favour of rich comparisons.

If you inherit from a base class that implements rich comparisons, you have
to override all those methods, else your __cmp__ won't be called at all.
Sometimes it may be enough to implement __cmp__ and make all others call it.
 
A

Antoon Pardon

I'm writing a class that implements rich comparisons, and I find myself
writing a lot of very similar code. If the calculation is short and
simple, I do something like this:


class Parrot:
def __eq__(self, other):
return self.plumage() == other.plumage()
def __ne__(self, other):
return self.plumage() != other.plumage()
def __lt__(self, other):
return self.plumage() < other.plumage()
def __gt__(self, other):
return self.plumage() > other.plumage()
def __le__(self, other):
return self.plumage() <= other.plumage()
def __ge__(self, other):
return self.plumage() >= other.plumage()

Well one thing you could do is write the following class:

Comparators = SomeEnumWith("eq, ne, lt, gt, ge, le, ge")

class GeneralComparator:
def __eq__(self, other):
return Comparators.eq in self.__compare__(self, other)
def __ne__(self, other):
return Comparators.ne in self.__compare__(self, other)
def __lt__(self, other):
return Comparators.lt in self.__compare__(self, other)
def __le__(self, other):
return Comparators.le in self.__compare__(self, other)
def __gt__(self, other):
return Comparators.gt in self.__compare__(self, other)
def __ge__(self, other):
return Comparators.ge in self.__compare__(self, other)


Then write your Parrot class as follows:

class Parrot (GeneralComparator):
def __compare__(self, other):
return a set which defines which comparisons should return true.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,756
Messages
2,569,533
Members
45,007
Latest member
OrderFitnessKetoCapsules

Latest Threads

Top