functools: Easy comparative operators in Python

I love object orientation, but for some things a functional programming style is really better. Python makes this pretty easy, and in the next posts I’d like to introduce some functions from the functools library that can modify your functions or methods to save you from doing a lot of programming push-ups. Today I’ll show you how to total_ordering decorator that gets all six comparative operators to work on your class with just two explicitly written, saving you the work of having to write the other four.

functools has a lot of great stuff, so if you like what you read here, take a look at the functools documentation to find a whole lot of other really cool functions. Today, I’ll only be talking about one: total_ordering.

total_ordering: No more busy work on comparative operators

If you write a custom class in Python, chances are that you might want to compare multiple instances of that class. To illustrate this, let’s take a look at a custom implementation of fractions as numbers in Python (I know that fractions are already implemented, I’m just using this is an example):

class Fraction:
"""A very simple implementation of fraction numbers"""
def __init__(self, numerator, denominator):
self.numerator = numerator
self.denominator = denominator

Now this stores your fractions with their numerators and denominators, nothing more. Pretty useless, but we see that it works:

>>> f1 = Fraction(1, 3)
>>> f2 = Fraction(2, 4)
>>> print(f1.numerator, f1.denominator)
(1, 3)
>>> print(f2.numerator, f2.denominator)
(2, 4)

Okay, so that works, the numbers are tucked nicely away. But what happens when we try to compare them?

>>> f1 > f2
True
>>> f2 > f1
False

Wait, f1 is 1/3 and f2 is 2/4, or 1/2 if we like reducing. 1/3 is 0.33, definitely not larger than 0.5. So obviously the comparison is working wrong. We need to tell Python explicitly how to compare instances of this class. Let’s do that:

class Fraction:
    """A very simple implementation of fraction numbers"""
    def __init__(self, numerator, denominator):
        self.numerator = numerator
        self.denominator = denominator

    def to_decimal(self):
        return float(self.numerator) / self.denominator

    def __lt__(self, other):
        return self.to_decimal() < other.to_decimal()     def __eq__(self, other):         return self.to_decimal() == other.to_decimal() 

This is a kind of ugly solution because it compares by converting to a decimal, but I’m wanting to demonstrate the methods more than the numerics. The __lt__() method performs the < operator and the __eq__() method performs ==. Now comparison works! Check it out:

 >>> f1, f2 = Fraction(1, 3), Fraction(2, 4)
>>> f1 < f2 True >>> f1 == f2
False
>>> f1 > f2
False

Not only does it work, but the greater than operator works as well! That’s because in Python, if you don’t define a greater than operator (__gt__), Python swaps the operands and uses __lt__. That’s awesome. But there’s one problem: It doesn’t know how to use < and == to combine into <=, nor can it turn that around and perform >=, even though technically all the information that you’d need for that are there. Check this out:

>>> f3 = Fraction(2, 6)
>>> f1 <= f3
False

The solution? If you want => and <= to work right without functools, it would be to write each and every operand by hand. A lot of boilerplating, considering that with either greater than or less than plus equal can tell you all the other operands’ results. functools takes care of that for you using the decorator @total_ordering. If you want to know how it works, check out the documentation. Using it is very, very simple – you just add the @total_ordering decorator before your class definition:

import functools

@functools.total_ordering
class Fraction:
    """A very simple implementation of fraction numbers"""
    def __init__(self, numerator, denominator):
        self.numerator = numerator
        self.denominator = denominator

    def to_decimal(self):
        return float(self.numerator) / self.denominator

    def __lt__(self, other):
        return self.to_decimal() < other.to_decimal()     def __eq__(self, other):         return self.to_decimal() == other.to_decimal() 

Now take a look at the results.

 >>> f1, f2, f3 = Fraction(1, 3), Fraction(2, 4), Fraction(2, 6)
>>> f1 < f2 True >>> f1 == f3
True
>>> f1 < f3 False >>> f1 > f3
False
>>> f1 <= f3
True

And that’s all there is to it!

Advertisements
About

My name’s Daniel Lee. I’m an enthusiast for open source and sharing. I grew up in the United States and did my doctorate in Germany. I've founded a company for planning solar power. I've worked on analog space suit interfaces, drones and a bunch of other things in my free time. I'm also involved in standards work for meteorological data. Now I work for the German Weather Service on improving forecasts for weather and renewable power production.

Tagged with: , ,
Posted in Uncategorized

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

From the archive