Overloading Comparison Operators in Python: Defining Magnitude Relationships (<, <=, ==, etc.) for Custom Classes

Just as you can compare numbers or strings with a < b or a == b, you may want to define magnitude relationships or equality for your own custom classes (objects).

For example, judging “Version 1.2 is older (smaller) than 1.5” in a class managing version numbers, or judging “Do the periods overlap?” in a date range class.

This article explains how to implement special methods corresponding to comparison operators and the functools.total_ordering decorator to simplify implementation.


目次

List of Special Methods for Comparison Operators

The special methods corresponding to each comparison operator are as follows:

OperatorSpecial MethodEnglish (Meaning)
==__eq__Equal
!=__ne__Not Equal
<__lt__Less Than
<=__le__Less than or Equal
>__gt__Greater Than
>=__ge__Greater than or Equal

Implementing all of these allows for complete comparison, but you don’t actually need to write them all.


Implementation Example: Software Version Comparison

As an example, let’s create a Version class that manages software versions (major.minor.patch).

Comparing 1.2.0 and 1.10.0 as strings results in “1.2.0” > “1.10.0”, but implementing it correctly as a class allows for comparison based on numerical magnitude.

Efficiency with functools.total_ordering

Python provides a convenient decorator @total_ordering (in the functools module).

Using this, if you define __eq__ and just one magnitude comparison (e.g., __lt__), Python will automatically complement the remaining operators (<=, >, >=).

from functools import total_ordering

@total_ordering
class Version:
    def __init__(self, major, minor, patch=0):
        self.major = major
        self.minor = minor
        self.patch = patch

    def __repr__(self):
        return f"v{self.major}.{self.minor}.{self.patch}"

    def _to_tuple(self):
        """Helper method to return a tuple for comparison"""
        return (self.major, self.minor, self.patch)

    # --- Mandatory: Equality check (==) ---
    def __eq__(self, other):
        if not isinstance(other, Version):
            return NotImplemented
        return self._to_tuple() == other._to_tuple()

    # --- Mandatory: One magnitude comparison (here <) ---
    def __lt__(self, other):
        if not isinstance(other, Version):
            return NotImplemented
        # Tuple comparison checks elements in order from the beginning
        # (1, 2, 0) < (1, 10, 0) -> True
        return self._to_tuple() < other._to_tuple()

# --- Verification ---

v1 = Version(1, 2, 5)
v2 = Version(1, 10, 0)
v3 = Version(1, 2, 5)

print(f"v1: {v1}")
print(f"v2: {v2}")
print(f"v3: {v3}")
print("-" * 20)

# Defined __eq__ and __lt__
print(f"v1 == v3 : {v1 == v3}")  # True
print(f"v1 < v2  : {v1 < v2}")   # True (1.2.5 < 1.10.0)

# Operators auto-generated by @total_ordering
print(f"v1 != v2 : {v1 != v2}")  # True
print(f"v2 > v1  : {v2 > v1}")   # True
print(f"v1 <= v3 : {v1 <= v3}")  # True

Output:

v1: v1.2.5
v2: v1.10.0
v3: v1.2.5
--------------------
v1 == v3 : True
v1 < v2  : True
v1 != v2 : True
v2 > v1  : True
v1 <= v3 : True

Explanation

  1. _to_tuple: We prepared a method to group attributes into a tuple (major, minor, patch) to simplify comparison logic. Python tuples automatically compare elements in order from the beginning, so we utilize this.
  2. __eq__: Judges whether they are equal.
  3. __lt__: Judges whether it is “less than.”
  4. @total_ordering: Based on the above two, it automatically generates the remaining __le__, __gt__, __ge__, __ne__.

This allows for intuitive object comparison with minimal code.


Another Example: Comparison of Shapes by Area

If the magnitude is determined by a single numerical value (scalar), the implementation becomes even simpler.

Let’s create a Circle class with a radius and enable comparison by area size.

import math

@total_ordering
class Circle:
    def __init__(self, radius):
        self.radius = radius

    @property
    def area(self):
        """Property to calculate area"""
        return self.radius * self.radius * math.pi

    def __repr__(self):
        return f"Circle(r={self.radius})"

    def __eq__(self, other):
        if not isinstance(other, Circle):
            return NotImplemented
        return self.radius == other.radius

    def __lt__(self, other):
        if not isinstance(other, Circle):
            return NotImplemented
        # Compare by radius (or area)
        return self.radius < other.radius

# --- Verification ---
c1 = Circle(5)
c2 = Circle(10)

# You can also use functions like sort()
circles = [c2, Circle(1), c1]
sorted_circles = sorted(circles)

print(f"Before sort: {circles}")
print(f"After sort:  {sorted_circles}")

Output:

Before sort: [Circle(r=10), Circle(r=1), Circle(r=5)]
After sort:  [Circle(r=1), Circle(r=5), Circle(r=10)]

A major benefit of implementing comparison operators is that sorted() functions and sort() methods for lists can be used as they are.


Summary

  • To use comparison operators (==, <, >, etc.) in custom classes, implement special methods like __eq__, __lt__.
  • Using the functools.total_ordering decorator, you only need to define __eq__ and one other (__lt__, etc.) to use all comparison operators.
  • This allows for intuitive object sorting and magnitude judgment.
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次