数値や文字列同士で a < b や a == b といった比較ができるように、自作のクラス(オブジェクト)同士でも大小関係や等価性を定義したい場合があります。
例えば、バージョン番号を管理するクラスで「バージョン1.2は1.5より古い(小さい)」と判定したり、日付範囲クラスで「期間が重なっているか」を判定したりするケースです。
この記事では、比較演算子に対応する特殊メソッドの実装方法と、実装を楽にする functools.total_ordering デコレータについて解説します。
比較演算子に対応する特殊メソッド一覧
各比較演算子に対応する特殊メソッドは以下の通りです。
| 演算子 | 特殊メソッド | 英語(意味) |
== | __eq__ | Equal (等しい) |
!= | __ne__ | Not Equal (等しくない) |
< | __lt__ | Less Than (より小さい) |
<= | __le__ | Less than or Equal (以下) |
> | __gt__ | Greater Than (より大きい) |
>= | __ge__ | Greater than or Equal (以上) |
これらをすべて実装すれば完全な比較が可能になりますが、実はすべてを書く必要はありません。
実装例:ソフトウェアのバージョン比較
例として、ソフトウェアのバージョン(メジャー.マイナー.パッチ)を管理する Version クラスを作成します。
1.2.0 と 1.10.0 を文字列として比較すると “1.2.0” > “1.10.0” となってしまいますが、クラスとして正しく実装すれば数値的な大小関係で比較できます。
functools.total_ordering による効率化
Pythonには便利なデコレータ @total_ordering が用意されています(functools モジュール)。
これを使用すると、__eq__ と、大小比較のいずれか一つ(例えば __lt__)を定義するだけで、残りの演算子(<=, >, >=)をPythonが自動的に補完してくれます。
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):
"""比較用のタプルを返すヘルパーメソッド"""
return (self.major, self.minor, self.patch)
# --- 必須: 等価判定 (==) ---
def __eq__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._to_tuple() == other._to_tuple()
# --- 必須: いずれか一つの大小比較 (ここでは < ) ---
def __lt__(self, other):
if not isinstance(other, Version):
return NotImplemented
# タプル同士の比較は、先頭の要素から順に比較される
# (1, 2, 0) < (1, 10, 0) -> True
return self._to_tuple() < other._to_tuple()
# --- 動作確認 ---
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)
# 定義した __eq__ と __lt__
print(f"v1 == v3 : {v1 == v3}") # True
print(f"v1 < v2 : {v1 < v2}") # True (1.2.5 < 1.10.0)
# @total_ordering が自動生成してくれた演算子
print(f"v1 != v2 : {v1 != v2}") # True
print(f"v2 > v1 : {v2 > v1}") # True
print(f"v1 <= v3 : {v1 <= v3}") # True
実行結果:
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
解説
_to_tuple: 比較ロジックを簡単にするため、属性をタプル(major, minor, patch)にまとめるメソッドを用意しました。Pythonのタプルは自動的に先頭の要素から順に比較を行ってくれるため、これを利用します。__eq__: 等しいかどうかを判定します。__lt__: 「より小さい」かどうかを判定します。@total_ordering: 上記2つを元に、残りの__le__,__gt__,__ge__,__ne__を自動的に生成します。
これにより、最小限のコード量で直感的なオブジェクト比較が可能になります。
別の例:面積による図形の比較
もし単一の数値(スカラー値)で大小が決まるようなオブジェクトであれば、実装はさらに単純になります。
例として、半径を持つ Circle(円)クラスを作成し、面積の大きさで比較できるようにします。
import math
@total_ordering
class Circle:
def __init__(self, radius):
self.radius = radius
@property
def area(self):
"""面積を計算するプロパティ"""
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
# 半径(または面積)の大小で比較
return self.radius < other.radius
# --- 動作確認 ---
c1 = Circle(5)
c2 = Circle(10)
# sort() 関数なども使えるようになる
circles = [c2, Circle(1), c1]
sorted_circles = sorted(circles)
print(f"ソート前: {circles}")
print(f"ソート後: {sorted_circles}")
実行結果:
ソート前: [Circle(r=10), Circle(r=1), Circle(r=5)]
ソート後: [Circle(r=1), Circle(r=5), Circle(r=10)]
比較演算子を実装することで、リストの sorted() 関数や sort() メソッドがそのまま利用できるようになるのも大きなメリットです。
まとめ
- 自作クラスで比較演算子(
==,<,>など)を使うには、__eq__,__lt__などの特殊メソッドを実装します。 functools.total_orderingデコレータを使うと、__eq__と__lt__(または他の一つ)を定義するだけで、すべての比較演算子を使えるようになります。- これにより、オブジェクトのソートや大小判定が直感的に行えるようになります。
