Why do I need total_ordering?

If you have a class and you need to compare objects to eachother, total_ordering will help save you from code duplication. If you supply an object with an __eq__ method and one of the other comparison methods, __le__, __lt__, __ge__, or __gt__, the remaining comparison methods will be "magically" supplied as well.

Example

Using the example from the functools documentation, let's see what happens if we don't use total_ordering:

class Student:
    def _is_valid_operand(self, other):
        return (hasattr(other, "lastname") and
                hasattr(other, "firstname"))
    def __eq__(self, other):
        if not self._is_valid_operand(other):
            return NotImplemented
        return ((self.lastname.lower(), self.firstname.lower()) ==
                (other.lastname.lower(), other.firstname.lower()))
    def __lt__(self, other):
        if not self._is_valid_operand(other):
            return NotImplemented
        return ((self.lastname.lower(), self.firstname.lower()) <
                (other.lastname.lower(), other.firstname.lower()))

sam = Student()
sam.firstname = 'Sam'
sam.lastname = 'Zuckerman'

john = Student()
john.firstname = 'John'
john.lastname = 'Smith'

The above class has only implemented equality and less-than methods.

sam == john

>>> False
sam < john

>>> False

Now, if I use any other comparison methods:

sam <= john

>>> TypeError: '<=' not supported between instances of 'Student' and 'Student'

Using the total_ordering decorator

If I decorate the above class with the total_ordering decorator, I won't throw a TypeError as above.

from functools import total_ordering

@total_ordering
class Student:
    def _is_valid_operand(self, other):
        return (hasattr(other, "lastname") and
                hasattr(other, "firstname"))
    def __eq__(self, other):
        if not self._is_valid_operand(other):
            return NotImplemented
        return ((self.lastname.lower(), self.firstname.lower()) ==
                (other.lastname.lower(), other.firstname.lower()))
    def __lt__(self, other):
        if not self._is_valid_operand(other):
            return NotImplemented
        return ((self.lastname.lower(), self.firstname.lower()) <
                (other.lastname.lower(), other.firstname.lower()))

sam = Student()
sam.firstname = 'Sam'
sam.lastname = 'Zuckerman'

john = Student()
john.firstname = 'John'
john.lastname = 'Smith'

sam <= john

>>> False

To implement the __le__ method I would have needed to copy and paste five lines of code and change one character. To implement all the methods, I would need about 15 additional lines of code. It's quite a waste of space which is saved by a simple decorator. In addition, it saves minor errors and typos that could come from manually needing to change small items in very similar looking methods.

Performance

In the functools documentation it mentions that if performance is a consideration for your application, you might want to just implement all the methods instead of using this decorator.

Note: While this decorator makes it easy to create well behaved totally ordered types, it does come at the cost of slower execution and more complex stack traces for the derived comparison methods. If performance benchmarking indicates this is a bottleneck for a given application, implementing all six rich comparison methods instead is likely to provide an easy speed boost.

Conclusion

The total_ordering decorator is an easy way to save on code duplication when writing classes that utilize comparison metrics. By writing an __eq__ method and another comparison method, all the work is done for you.

Comments

comments powered by Disqus

Published

Category

Python

Tags

Contact