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:
| Operator | Special Method | English (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
_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.__eq__: Judges whether they are equal.__lt__: Judges whether it is “less than.”@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_orderingdecorator, 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.
