Python環境を起動中... 初回のみ数秒かかります
Lesson 15 / 20

特殊メソッド

このレッスンで学ぶこと

  • __str____repr__ で表示をカスタマイズできる
  • __len____getitem__ でコンテナのように振る舞えるクラスを作れる
  • __eq__ / __lt__ で比較演算子を定義できる
  • __add__ などで算術演算子をオーバーロードできる

1. __str__ と __repr__

特殊メソッド(ダンダーメソッド)とは __名前__ の形をした、Python が自動で呼び出す特別なメソッドです。 print()len()+ などの組み込み操作は、内部でこれらの特殊メソッドを呼び出すことで動いています。

🔑 特殊メソッドを定義する理由: 自作クラスのオブジェクトに対して print()len()+ などの標準的な書き方を使えるようにするためです。

定義しなければどうなるか? — __str__ のない Patient クラスで print(p) すると次のように表示されます:

<__main__.Patient object at 0x7f4a2c3b0d90>  ← メモリアドレス(人には読めない)

__str__ を定義することで、print(p) が自動的にそのメソッドを呼び出し「患者: 田中太郎(45歳)」のような読みやすい表示に変わります。

__str__print(obj)str(obj) で呼ばれるユーザー向けの表示、 __repr__repr(obj) で呼ばれる開発者向けの詳細表示です。 リストに入れて print() したときは __repr__ が使われます。

sample_1.py
Ctrl+Enter
出力

💡 内部の呼び出しの仕組み:

print(p)       →  p.__str__()  を自動で呼び出す
str(p)         →  p.__str__()  を呼び出す
repr(p)        →  p.__repr__() を呼び出す
print([p1,p2]) →  各要素の __repr__() を呼び出す

自分で p.__str__() と書かなくても、print(p) と書くだけで Python が裏側で代わりに呼んでくれます。__str__ がない場合は __repr__ が代用され、それもない場合はメモリアドレス表示になります。

2. __len__ と __getitem__

__len__ を定義すると len(obj) が使えます。 __getitem__ を定義すると obj[i] のインデックスアクセスができます。

sample_2.py
Ctrl+Enter
出力

ここで重要なことに気づきます。今まで当たり前のように使ってきた len(my_list)my_list[0] も、実は内部で特殊メソッドを呼び出していたのです。

🎉 len() と [] の謎が解けた!

len([1, 2, 3])    →  [1, 2, 3].__len__()    を呼んでいた!
my_list[0]        →  my_list.__getitem__(0)  を呼んでいた!
"abc"[1]          →  "abc".__getitem__(1)    を呼んでいた!

組み込みの liststrtuple などには、Python 開発者があらかじめ __len____getitem__ を実装しています。だから最初から len()[] が使えたのです。

💡 「len() や [] はもともと使えるのに、なぜ自作クラスでも定義が必要?」

組み込みの liststr には Python 開発者がすでに __len__ などを実装しています。ところが自作の PatientList クラスは Python が初めて見るクラスです。「何個入っているのか」「どうインデックスにアクセスするのか」を Python は知る方法がありません。

__len__ を定義することで「このクラスの長さは self._data の長さですよ」と Python に教えます。これが Python の プロトコル(duck typing) という設計思想で、「__len__ を持っていれば長さを持つもの、__getitem__ を持っていればインデックスアクセスできるもの」とみなします。

3. 比較演算子のオーバーロード

__eq____lt____le__ などを定義すると、 ==<<= などの演算子が使えます。 sorted() などの組み込み関数とも連動します。

__eq__ を定義しない場合は、== が「同じオブジェクトかどうか(メモリアドレスが同じか)」を比較します。 つまり中身が全く同じでも別々に作ったオブジェクト同士は False になります。 __eq__ を定義することで「どの属性が一致すれば等しいとみなすか」を自分で決められます。

📋 定義前 vs 定義後の違い:

p1 = Patient("田中太郎", 45)
p2 = Patient("田中太郎", 45)

# __eq__ なし → False(別々に作ったオブジェクトだから)
# __eq__ あり → True(name と age が一致するから)
sample_3.py
Ctrl+Enter
出力

📋 比較演算子と対応する特殊メソッド:

__eq__(self, other)   →  a == b
__ne__(self, other)   →  a != b
__lt__(self, other)   →  a  < b  (sorted() はこれだけで動く)
__le__(self, other)   →  a <= b
__gt__(self, other)   →  a  > b
__ge__(self, other)   →  a >= b

__lt__ を定義するだけで sorted() が使えます。6つ全部定義したい場合は from functools import total_ordering デコレータを使うと、__eq__ と1つの不等号メソッドだけ書けば残りが自動補完されます。

4. 算術演算子のオーバーロード

__add____mul____sub__ などで +*- 演算子を定義できます。 PyTorch の Tensora + ba * 2 で演算できるのもこの仕組みです。

a + b を書くと Python は a.__add__(b) を呼び出します。 __rmul__ については、コードを動かした後の補足で解説します。

sample_4.py
Ctrl+Enter
出力

💡 なぜ __rmul__ が必要なのか:

vector * 22 * vector はどちらも同じ結果ですが、Pythonが内部で試みる順番が違います。

書き方Pythonが試みること結果
vector * 2vector.__mul__(2) を呼ぶ__mul__ を定義済みなので動く
2 * vector① まず int.__mul__(2, vector) を試みる
② int は Vector を知らないので失敗
③ 次に vector.__rmul__(2) を試みる
__rmul__ があれば動く
❌ なければ TypeError

__rmul__(right multiply)は「* 演算子の右側のオペランドになったときのかけ算」用の予備メソッドです(2 * vector では vector* の右側にあります)。今回は return self.__mul__(scalar) と書くだけで __mul__ に処理を任せられます。

📋 算術演算子と対応する特殊メソッド:

__add__(self, other)      →  a + b
__sub__(self, other)      →  a - b
__mul__(self, other)      →  a * b
__truediv__(self, other)  →  a / b
__floordiv__(self, other) →  a // b
__mod__(self, other)      →  a % b
__rmul__(self, other)     →  b * a  (左辺が未対応のときのフォールバック)

💡 PyTorch文脈: tensor_a + tensor_btensor * 2.0tensor @ weight(行列積)はすべてこの仕組みで動いています。NumPy や PyTorch の配列演算がなぜあれほど直感的に書けるのか、その答えがここにあります。

5. 特殊メソッドは全部定義する必要があるの?

ここまで多くの特殊メソッドを見てきましたが、「クラスを作るたびに全部定義しなければいけないの?」と思うかもしれません。 答えは「必要なものだけ定義すればOK」です。

len()[]+・比較演算子など多くの操作は、対応する特殊メソッドが定義されていないと TypeError になります (例:__len__ がないクラスで len() を使うと TypeError: object of type '...' has no len())。 ただし __eq__(未定義時はオブジェクトの同一性で比較)や __str__/__repr__(未定義時はデフォルト表示にフォールバック)など、 Python がデフォルト動作を提供する特殊メソッドもあります。 使いたい操作に対応するメソッドだけを定義すれば十分で、使わない操作のメソッドは定義しなくてかまいません。

特殊メソッドいつ定義する?定義しないと?
__init__ほぼ必ず(初期化したい値があれば)引数なしで作られる
__str__ / __repr__デバッグ・表示を読みやすくしたいとき<ClassName object at 0x...> と表示される
__len__ / __getitem__コンテナ(リストのような)クラスを作るときlen() / [] が使えない
__eq__ / __lt__==sorted() で比較させたいときメモリアドレスで比較される
__add__数値的・ベクトル的な演算をさせたいとき+ などが使えない

💡 実際のコードでの頻度:
普通のデータクラス(患者情報・設定データなど)なら __init____str__ の2つだけで十分なことがほとんどです。
__len__ や演算子オーバーロードは「リストのように使えるクラス」「NumPy配列のように計算できるクラス」など、 特定の用途を持つクラスに限定して使います。
「使いたい操作が出てきたときに追加する」くらいのスタンスで大丈夫です。

6. 練習問題

問題 1

__str__ の実装

Patient クラスに __str__ を追加し、print(p) で「田中太郎さん(45歳)/ 診断: 高血圧」と表示されるようにしてください。

exercise_1.py
出力
ヒントを見る(答え+解説)
class Patient:
    def __init__(self, name, age, diagnosis):
        self.name = name
        self.age = age
        self.diagnosis = diagnosis

    def __str__(self):
        return f"{self.name}さん({self.age}歳)/ 診断: {self.diagnosis}"

p = Patient("田中太郎", 45, "高血圧")
print(p)
# → 田中太郎さん(45歳)/ 診断: 高血圧

__str__print()str() で呼ばれる特殊メソッドです。f-string で整形した文字列を return するだけで、オブジェクトを人が読みやすい形で表示できます。

問題 2

患者リストに __len__ と __getitem__ を追加

下記の Ward クラスに __len____getitem__ を実装して、len(ward)ward[0] が動くようにしてください。

exercise_2.py
出力
ヒントを見る(答え+解説)
class Ward:
    def __init__(self, name):
        self.name = name
        self.patients = []

    def admit(self, patient_name):
        self.patients.append(patient_name)

    def __len__(self):
        return len(self.patients)

    def __getitem__(self, index):
        return self.patients[index]

w = Ward("4階東病棟")
w.admit("田中太郎")
w.admit("山田花子")
w.admit("佐藤次郎")

print(f"入院患者数: {len(w)}名")
print(f"最初の患者: {w[0]}")
print(f"最後の患者: {w[-1]}")
# → 入院患者数: 3名 / 最初の患者: 田中太郎 / 最後の患者: 佐藤次郎

__len__ を実装すると len() が使え、__getitem__ を実装するとインデックスアクセス obj[i] が使えます。自作クラスをリストのように扱えるようになります。

問題 3

年齢で比較できるPatientクラス

Patient クラスに __lt__ を実装して sorted() で年齢昇順に並べ替えできるようにしてください。

exercise_3.py
出力
ヒントを見る(答え+解説)
class Patient:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __repr__(self):
        return f"{self.name}({self.age})"

    def __lt__(self, other):
        return self.age < other.age

patients = [
    Patient("田中太郎", 45),
    Patient("山田花子", 62),
    Patient("佐藤次郎", 33),
    Patient("鈴木一郎", 28),
]

print("年齢昇順:", sorted(patients))
print("年齢降順:", sorted(patients, reverse=True))
# → 年齢昇順: [鈴木一郎(28), 佐藤次郎(33), 田中太郎(45), 山田花子(62)]
# → 年齢降順: [山田花子(62), 田中太郎(45), 佐藤次郎(33), 鈴木一郎(28)]

__lt__(less than)を実装すると sorted()< 演算子で比較できるようになります。return self.age < other.age の1行だけで年齢順のソートが可能になります。

まとめ

このレッスンのポイント

  • __str__print(obj) の表示をカスタマイズ(ユーザー向け)
  • __repr__repr(obj) やリスト表示(開発者向け)
  • __len__len(obj)__getitem__obj[i] を有効化
  • __eq__==__lt__<sorted() と連動)
  • __add__, __mul__ → 算術演算子を定義(PyTorchのTensorと同じ仕組み)

自由に試してみましょう:

free_practice.py
Ctrl+Enter
出力

完了するとトップページに進捗が表示されます