Paul Fornia
Paul Fornia

Reputation: 452

functools total_ordering doesn't appear to do anything with inherited class

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

Answers (2)

Silvio Mayolo
Silvio Mayolo

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

Paul M.
Paul M.

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

Related Questions