python - native approach to comparing objects - python

I have a simple python class, that I want to be able to compare. So I implemented compare operators. I then realized that I've been doing that same thing for so many classes, and it feels a lot like code duplication.
class Foo(object):
def __init__(self, index, data):
self.index = index
self.data = data
def __lt__(self, other):
return self.index < other.index
def __gt__(self, other):
return self.index > other.index
def __le__(self, other):
return self.index <= other.index
def __ge__(self, other):
return self.index >= other.index
def __eq__(self, other):
return self.index == other.index
def __ne__(self, other):
return self.index != other.index
So I think a simple solution would be something like this:
class Comparable(object):
def _compare(self, other):
raise UnimplementedError()
def __lt__(self, other):
return self._compare(other) < 0
def __gt__(self, other):
return self._compare(other) > 0
def __le__(self, other):
return self._compare(other) <= 0
def __ge__(self, other):
return self._compare(other) >= 0
def __eq__(self, other):
return self._compare(other) == 0
def __ne__(self, other):
return self._compare(other) != 0
class Foo1(Comparable):
def _compare(self, other):
return self.index - other.index
class Foo2(Comparable):
def _compare(self, other):
# ...
class Foo3(Comparable):
def _compare(self, other):
# ...
But it seems so basic, that I feel like I'm reinventing the wheel here.
I'm wondering if there a more 'native' way to achieve that.

As described in the docs you can use functools.total_ordering to save some boilerplate in writing all of the comparisons
To avoid the hassle of providing all six functions, you can implement __eq__, __ne__, and only one of the ordering operators, and use the functools.total_ordering() decorator to fill in the rest.
To be explicit, the six functions they are referring to are: __eq__, __ne__, __lt__, __le__, __gt__, and __ge__.

So, you want some automation while creating rich comparison methods. You can have this behaviour by using functools.total_ordering() higher-order function. See the reference for more details.

Related

Cannot implement __lt__ operator

I have a class Quantity and I want to implement the __lt__ operator to overload. I have implemented the method as shown below.
def __lt__(self, other):
if isinstance(other, Quantity):
return other.value < self.value
if isinstance(other, numbers.Real):
return self.value < other
raise NotImplemented
When I import it into the python console and try to execute it
>>> Quantity(1.2) > 1
I get the following error
AttributeError: 'int' object has no attribute 'number'
And if I try to execute it like this
>>> Quantity(1.2) < 1
I get the following error
TypeError: '<' not supported between instances of 'Quantity' and 'int'
Does anyone know how can I implement the overload to work with < and >?
Update
Here is the full class
class Quantity:
def __init__(self, value):
self.value = float(value)
def __float__(self):
return self.value
def __repr__(self):
return 'Quantity({})'.format(self.value)
def __str__(self):
return str(self.value)
def __lt__(self, other):
if isinstance(other, Quantity):
return other.value < self.value
if isinstance(other, numbers.Real):
return self.value > other
raise NotImplemented
#property
def value(self):
return self._value
#value.setter
def value(self, value):
value = round(value, 10)
self._value = int(value * 1000) / 1000.0
You get an error on Quantity(1.2) > 1 because you have not defined __gt__ for your Quantity class. As pointed out, you also have an error in your __lt__ method and perform the comparison the wrong way around (other.value < self.value rather than self.value < other.value). I believe this should work:
def __lt__(self, other):
if isinstance(other, Quantity):
return self.value < other.value
if isinstance(other, numbers.Real):
return self.value < other
raise NotImplemented
def __gt__(self, other):
if isinstance(other, Quantity):
return self.value > other.value
if isinstance(other, numbers.Real):
return self.value > other
raise NotImplemented
Your second example, Quantity(1.2) < 1 should work. The error message you quote on that one is not reproducible, it seems (I checked python 2.7 and 3.8).

How to write a custom comparator with custom sort in Python3 to use it in sorted() function

I'm currently stuck on a problem to write a comparator. The base idea was to write a function, which takes to parameters (two lists), but I want to use it on a list of these lists to use it in sorted() function. How shall I do it?
Comparator:
def dispersion_sort(frec, srec):
if isinstance(frec, intervals.Interval) and isinstance(srec, intervals.Interval):
if frec[DOUBLE_RES_COL] < srec[DOUBLE_RES_COL]:
return frec
if frec[DOUBLE_RES_COL] > srec[DOUBLE_RES_COL]:
return srec
if frec[DOUBLE_RES_COL].overlaps(srec[DOUBLE_RES_COL]):
if (frec[DOUBLE_TIME_COL] < srec[DOUBLE_TIME_COL]):
return frec
else:
return srec
return frec
Sample frec data:
['1', 'Mikhail Nitenko', '#login', '✅', [-0.000509228437634554,0.0007110924383354339], datetime.datetime(2020, 1, 2, 14, 46, 46)]
How I wanted to call it:
results = sorted(results, key=dispersion_sort)
Thanks a lot!
You can use functools.cmp_to_key for this:
from functools import cmp_to_key
results = sorted(results, key=cmp_to_key(dispersion_sort))
It will transform the old style comparator function (which takes two arguments), into a new style key function (which takes one argument).
If you wanted to explicitly create a comparator, you'd want to implement a custom class that has that has these magic methods:
class comparator:
def __init__(self, obj, *args):
self.obj = obj
def __lt__(self, other):
return mycmp(self.obj, other.obj) < 0
def __gt__(self, other):
return mycmp(self.obj, other.obj) > 0
def __eq__(self, other):
return mycmp(self.obj, other.obj) == 0
def __le__(self, other):
return mycmp(self.obj, other.obj) <= 0
def __ge__(self, other):
return mycmp(self.obj, other.obj) >= 0
def __ne__(self, other):
return mycmp(self.obj, other.obj) != 0
Here, the function mycmp is a function like the one you showed. You can also choose to put your logic directly in the class itself. Here, these methods should return a True or False, which is different from your current function. Make sure that is changed accordingly if you want to use the current function directly into this class template.
Once you have the class ready , you can pass it in directly: key=comparator

Single function comparison in custom Python classes

I often need to make a class I made comparable for various reasons (sorting, set usage,...) and I don't want to write multiple comparison functions each time. How do I support the class so I only have to write a single function for every new class?
My solution to the problem is to create an abstract class a class can inherit and override the main comparison function (diff()) with the desired comparison method.
class Comparable:
'''
An abstract class that can be inherited to make a class comparable and sortable.
For proper functionality, function diff must be overridden.
'''
def diff(self, other):
"""
Calculates the difference in value between two objects and returns a number.
If the returned number is
- positive, the value of object a is greater than that of object b.
- 0, objects are equivalent in value.
- negative, value of object a is lesser than that of object b.
Used in comparison operations.
Override this function."""
return 0
def __eq__(self, other):
return self.diff(other) == 0
def __ne__(self, other):
return self.diff(other) != 0
def __lt__(self, other):
return self.diff(other) < 0
def __le__(self, other):
return self.diff(other) <= 0
def __gt__(self, other):
return self.diff(other) > 0
def __ge__(self, other):
return self.diff(other) >= 0

Min/Max Heap implementation in Python

This is my implementation of a MinHeap and MaxHeap in python. This uses a comparator to reverse the sequence of storage in the MaxHeap
import heapq
class MinHeap:
def __init__(self):
self.heap = []
def push(self, item):
heapq.heappush(self.heap, item)
def pop(self):
return heapq.heappop(self.heap)
def peek(self):
return self.heap[0]
def __getitem__(self, item):
return self.heap[item]
def __len__(self):
return len(self.heap)
class MaxHeap(MinHeap):
def push(self, item):
heapq.heappush(self.heap, Comparator(item))
def pop(self):
return heapq.heappop(self.heap)
def peek(self):
return self.heap[0]
def __getitem__(self, i):
return self.heap[i].val
class Comparator:
def __init__(self, val):
self.val = val
def __lt__(self, other):
return self.val > other
def __eq__(self, other):
return self.val == other
if __name__ == '__main__':
max_heap = MaxHeap()
max_heap.push(12)
max_heap.push(3)
max_heap.push(17)
print(max_heap.pop())
The MinHeap seems to work fine, however the MaxHeap throw up the following error.
<__main__.Comparator object at 0x10a5c1080>
I don't quite seem to understand what am I doing wrong here. Can someone help me with this.
I've added __repr__ and __gt__ methods to your Comparator class, so the code now runs, and the Comparator instances display their val when printed.
The important thing is to get those comparison methods to do the comparisons correctly between two Comparator instances.
You'll notice that I've eliminated most of the methods from MaxHeap. They aren't needed because the methods inherited from MinHeap work ok. You may wish to restore this one to MaxHeap
def __getitem__(self, i):
return self.heap[i].val
depending on how you intend to use MaxHeap.
import heapq
class MinHeap:
def __init__(self):
self.heap = []
def push(self, item):
heapq.heappush(self.heap, item)
def pop(self):
return heapq.heappop(self.heap)
def peek(self):
return self.heap[0]
def __getitem__(self, item):
return self.heap[item]
def __len__(self):
return len(self.heap)
class MaxHeap(MinHeap):
def push(self, item):
heapq.heappush(self.heap, Comparator(item))
class Comparator:
def __init__(self, val):
self.val = val
def __lt__(self, other):
return self.val > other.val
def __eq__(self, other):
return self.val == other.val
def __repr__(self):
return repr(self.val)
if __name__ == '__main__':
max_heap = MaxHeap()
max_heap.push(12)
max_heap.push(3)
max_heap.push(17)
while True:
try:
print(max_heap.pop())
except IndexError:
# The heap's empty, bail out
break
output
17
12
3
It's probably a Good Idea to give Comparator the full set of rich comparison methods. They aren't needed to make the above code work, but they will make the Comparator instances more flexible. So in case you want them, here they are:
def __lt__(self, other):
return self.val > other.val
def __le__(self, other):
return self.val >= other.val
def __gt__(self, other):
return self.val < other.val
def __ge__(self, other):
return self.val <= other.val
def __eq__(self, other):
return self.val == other.val
def __ne__(self, other):
return self.val != other.val

Implement a list wrapper with overridden __cmp__ function

I have created a new Python object as follows
class Mylist(list):
def __cmp__(self,other):
if len(self)>len(other):
return 1
elif len(self)<len(other):
return -1
elif len(self)==len(other):
return 0
my intend is, when two Mylist objects are compared the object with large number of items should be higher.
c=Mylist([4,5,6])
d=Mylist([1,2,3])
after running the above code, c and d are supposed to be equal(c==d <==True). But I am getting
>>> c==d
False
>>> c>d
True
>>>
they are being compared like the list object itself. What did I do wrong?
You need to implement function __eq__.
class Mylist(list):
def __cmp__(self,other):
if len(self)>len(other):
return 1
elif len(self)<len(other):
return -1
elif len(self)==len(other):
return 0
def __eq__(self, other):
return len(self)==len(other)
UPDATE: (previous code does not work perfectly as explained in comments)
Although #tobias_k answer explains it better, you can do it via __cmp__ function in Python 2 if you insist. You can enable it by removing other compare functions (le,lt,ge, ...):
class Mylist(list):
def __cmp__(self,other):
if len(self)>len(other):
return 1
elif len(self)<len(other):
return -1
elif len(self)==len(other):
return 0
def __eq__(self, other):
return len(self)==len(other)
#property
def __lt__(self, other): raise AttributeError()
#property
def __le__(self, other): raise AttributeError()
#property
def __ne__(self, other): raise AttributeError()
#property
def __gt__(self, other): raise AttributeError()
#property
def __ge__(self, other): raise AttributeError()
The problem seems to be that list implements all of the rich comparison operators, and __cmp__ will only be called if those are not defined. Thus, it seems like you have to overwrite all of those:
class Mylist(list):
def __lt__(self, other): return cmp(self, other) < 0
def __le__(self, other): return cmp(self, other) <= 0
def __eq__(self, other): return cmp(self, other) == 0
def __ne__(self, other): return cmp(self, other) != 0
def __gt__(self, other): return cmp(self, other) > 0
def __ge__(self, other): return cmp(self, other) >= 0
def __cmp__(self, other): return cmp(len(self), len(other))
BTW, it seems like __cmp__ was removed entirely in Python 3. The above works in Python 2.x, but for compatibility you should probably rather do it like
def __lt__(self, other): return len(self) < len(other)
Also see these two related questions. Note that while in Python 3 it would be enough to implement __eq__ and __lt__ and have Python infer the rest, this will not work in this case, since list already implements all of them, so you have to overwrite them all.

Categories