Reputation: 452
I am trying sort a list of strings in a way that uses a special comparison. I am trying to use functools.total_ordering
, but I'm not sure whether it's filling out the undefined comparisons correctly.
The two I define ( > and ==) work as expected, but < does not. In particular, I print all three and I get that a > b
and a < b
. How is this possible? I would think that total_ordering would simply define <
as not > and not ==
. The result of my <
test is what you would get with regular str comparison, leading me to believe that total_ordering isn't doing anything.
Perhaps the problem is that I am inheriting str, which already has __lt__
implemented? If so, is there a fix to this issue?
from functools import total_ordering
@total_ordering
class SortableStr(str):
def __gt__(self, other):
return self+other > other+self
#Is this necessary? Or will default to inherited class?
def __eq__(self, other):
return str(self) == str(other)
def main():
a = SortableStr("99")
b = SortableStr("994")
print(a > b)
print(a == b)
print(a < b)
if __name__ == "__main__":
main()
OUTPUT:
True
False
True
Upvotes: 1
Views: 1520
Reputation: 70347
You're right that the built-in str
comparison operators are interfering with your code. From the docs
Given a class defining one or more rich comparison ordering methods, this class decorator supplies the rest.
So it only supplies the ones not already defined. In your case, the fact that some of them are defined in a parent class is undetectable to total_ordering
.
Now, we can dig deeper into the source code and find the exact check
roots = {op for op in _convert if getattr(cls, op, None) is not getattr(object, op, None)}
So it checks if the values are equal to the ones defined in the root object object
. We can make that happen
@total_ordering
class SortableStr(str):
__lt__ = object.__lt__
__le__ = object.__le__
__ge__ = object.__ge__
def __gt__(self, other):
return self+other > other+self
#Is this necessary? Or will default to inherited class?
def __eq__(self, other):
return str(self) == str(other)
Now total_ordering
will see that __lt__
, __le__
, and __ge__
are equal to the "original" object
values and overwrite them, as desired.
This all being said, I would argue that this is a poor use of inheritance. You're violating Liskov substitution at the very least, in that mixed comparisons between str
and SortableStr
are going to, to put it lightly, produce counterintuitive results.
My more general recommendation is to favor composition over inheritance and, rather than defining a thing that "is a" specialized string, consider defining a type that "contains" a string and has specialized behavior.
@total_ordering
class SortableStr:
def __init__(self, value):
self.value = value
def __gt__(self, other):
return self.value + other.value > other.value + self.value
def __eq__(self, other):
return self.value == other.value
There, no magic required. Now SortableStr("99")
is a valid object that is not a string but exhibits the behavior you want.
Upvotes: 5
Reputation: 10809
Not sure if this is correct, but glancing at the documentation of functools.total_ordering
, this stands out to me:
Given a class defining one or more rich comparison ordering methods, this class decorator supplies the rest.
Emphasis mine. Your class inherits __lt__
from str
, so it does not get re-implemented by total_ordering
since it isn't missing. That's my best guess.
Upvotes: 1