順番に読み進めながら学べます

特殊メソッド — + や print の挙動をクラスに教える

Python の特殊メソッド(ダンダーメソッド)を解説します。__add__ で + をカスタマイズし、__str__ で print の表示を整え、__eq__ で == の比較を定義する流れまでを図解で押さえます。

前回まででインスタンスメソッド・クラスメソッド・スタティックメソッドの 3 種類のメソッドを整理しました。ここからは特殊なメソッド — 特殊メソッド(ダンダーメソッド) — を解説します。

特殊メソッドとは — 「ダンダーメソッド」

Python のクラスでよく見かける __init____str__ のように、前後をアンダースコア 2 つで囲んだ名前のメソッドを 特殊メソッド と呼びます。アンダースコア(dunder = double underscore)に挟まれているため、ダンダーメソッド とも呼ばれます。

特殊メソッドの大きな特徴は、自分で直接呼び出すものではなく、Python が特定の操作をしたときに自動で呼んでくれる点です。たとえば v1 + v2 と書いたとき、Python は内部で v1.__add__(v2) を呼びに行きます。print(p) を書いたら p.__str__() が呼ばれ、a == b を書いたら a.__eq__(b) が呼ばれます。

演算子と特殊メソッドの対応
コードの書きかたPython が変換呼び出されるメソッドv1 + v2__add__print(p)__str__a == b__eq__
Python の演算子や組み込み関数は、裏で対応するダンダーメソッドを呼び出している。クラス側でそのメソッドを定義しておけば、自作クラスでも +== が使えるようになる。

__add__ で + 演算子を定義する

代表例として + を見てみます。たとえば「金額(円)を表す Money クラス」を作ったとして、Money(300) + Money(500) と書いて Money(800) を作りたい — ということがあります。何もせずに足し算しようとすると、Python は「Money 同士の足し方を知らない」と言って TypeError を出してしまいます。

この「足し方」をクラスに教えるのが __add__ メソッドです。def __add__(self, other): を定義して、自分(self)と相手(other)から新しいインスタンスを作って返すように書けば、+ 演算子で使えるようになります。

class Money:
    def __init__(self, amount):
        self.amount = amount

    def __add__(self, other):                 # + のときに呼ばれる
        return Money(self.amount + other.amount)

wallet  = Money(300)
payment = Money(500)
total   = wallet + payment                    # 内部的には wallet.__add__(payment)
print(total.amount)                           # 800
v1 + v2 の裏で起きていること
wallet+ paymentwallet.__add__(payment)self =walletother =paymentreturn Money(self.amount + other.amount)Money(800)を返す変換
+ を書くと、Python が自動で self.__add__(other) を呼ぶ。self には左辺、other には右辺のインスタンスが入る。返した新しいインスタンスがそのまま + の結果になる。

Money クラスに __add__ を実装して、+ で残高を合算できるようにします。

class Money: を定義し、__init__(self, amount)self.amount = amount を代入してください。

__add__(self, other) を定義し、return Money(self.amount + other.amount) を返してください。

wallet = Money(300)payment = Money(500) を作り、total = wallet + paymenttotal.amountprint してください。

(正しく実行できれば解説が表示されます)

Python エディタ

コードを実行してください

__str__ と __repr__ — 文字列表現を 2 種類用意する

次は 「インスタンスを文字列にする」 ときに呼ばれる特殊メソッドです。これには 2 つあって、目的が違います。

- __str__print(p)str(p) のときに呼ばれる、ユーザー向けの見やすい文字列

- __repr__ — 対話シェルや開発者がデバッグ時に見たい、コードに近い形の詳細な文字列

何もしないと print(user)<__main__.User object at 0x...> のような分かりづらい結果になります。__str__ を定義すると見やすい表示になり、さらに __repr__ も定義しておくとデバッグの際に型と中身が一目で分かるようになります。

class User:
    def __init__(self, name, age):
        self.name = name
        self.age  = age

    def __str__(self):                       # print() / str() のとき
        return f"{self.name}{self.age}歳)"

    def __repr__(self):                      # 開発者向けデバッグ用
        return f"User(name={self.name!r}, age={self.age})"

u = User("花子", 30)
print(u)             # 花子(30歳)          ← __str__
print(repr(u))       # User(name='花子', age=30) ← __repr__
__str__ と __repr__ の使い分け
__str__(ユーザー向け)
  • print(u)str(u) のときに呼ばれる
  • 目的: エンドユーザーに見せる文字列
  • 例: 花子(30歳)
__repr__(開発者向け)
  • repr(u) や対話シェルでの表示に呼ばれる
  • __str__ が無いときの print でも代用される
  • 目的: 型と中身がはっきり分かるデバッグ表示
  • 例: User(name='花子', age=30)
両方定義するのが理想だが、最低限 __repr__ だけは書いておくとデバッグが楽になる。

User クラスに __str____repr__ を実装して、print 結果と repr 結果を比べます。

class User: を定義し、__init__(self, name, age)self.name / self.age を代入してください。

__str__(self) を定義し、return f"{self.name}({self.age}歳)" を返してください。

__repr__(self) を定義し、return f"User(name={self.name!r}, age={self.age})" を返してください。{self.name!r}!r は、値に対して repr() を呼んだ結果を埋め込む 書き方で、文字列なら '花子' のようにシングルクォート付きで出ます({self.name} だと 花子)。

u = User("花子", 30) を作り、print(u)print(repr(u)) の両方を実行して、表示の違いを確認してください。

Python エディタ

コードを実行してください

__eq__ で == の比較を定義する

次は等価性を見てみます。a == b と書いたとき、Python は内部的に a.__eq__(b) を呼びますが、自作クラスで __eq__ を定義していないと、デフォルトでは 「同じメモリ上のオブジェクトか」id の比較)になってしまいます。

たとえばクーポンを表す Coupon クラスがあって、コードが同じならクーポンとしては同じものとして扱いたい — そういう「業務上の等価性」を定義するのが __eq__ の仕事です。

__eq__ の有無で == の挙動が変わる
c1 == c2__eq__なしid 比較(メモリ位置)__eq__ありself.code== other.codeデフォルト定義時
__eq__定義しないと id 比較になり、中身が同じでも別インスタンスなら False__eq__定義すると 中身(code)で比較できるようになる。
class Coupon:
    def __init__(self, code, discount):
        self.code     = code
        self.discount = discount

    def __eq__(self, other):
        return self.code == other.code        # コードが同じなら同じクーポン

c1 = Coupon("SPRING10", 0.10)
c2 = Coupon("SPRING10", 0.20)               # 割引率は違うがコードは同じ
c3 = Coupon("SUMMER15", 0.15)

print(c1 == c2)   # True  (code が一致)
print(c1 == c3)   # False (code が違う)

__eq__ を定義しないとどうなる?

__eq__ を定義しないクラスでは、c1 == c2同一オブジェクトかどうか(メモリ上の同じ箱を指しているか)で判定されます。たとえコード・割引率がどちらも完全に同じでも、別々に作ったインスタンスは別のメモリ上に置かれるので結果は False になります。「中身で比較したい」場合は __eq__ を必ず自分で書きましょう。

Coupon クラスに __eq__ を実装して、コード(code)が同じなら等しい と判定するようにします。

class Coupon: を定義し、__init__(self, code, discount)self.code / self.discount を代入してください。

__eq__(self, other) を定義し、return self.code == other.code を返してください。

Coupon("SPRING10", 0.10)Coupon("SPRING10", 0.20)Coupon("SUMMER15", 0.15) の 3 つを作り、== で 2 通りの比較結果を print してください。

Python エディタ

コードを実行してください

他にもよく使う特殊メソッド — __call__ / __len__ / __bool__

ここまで紹介した 4 つ以外にも、よく使う特殊メソッドがいくつかあります。中でも代表的な 3 つを押さえておきましょう。

- __call__ — インスタンスを 関数のように () 付きで呼び出せる ようにする

- __len__len(obj) を呼んだときの返り値を決める

- __bool__if obj:bool(obj)真偽値として評価される値を決める

どれも組み込みの操作 — 関数呼び出し、len()if の条件評価 — を自作クラスに教えるためのメソッドです。アクセスログを溜めていく Logger クラスを例にすると、これら 3 つを 1 つのクラスに同居させて使い分けるイメージがつかみやすくなります。

class Logger:
    def __init__(self, name):
        self.name = name
        self.log  = []

    def __call__(self, message):                    # logger("...") で呼べる
        self.log.append(message)
        return f"[{self.name}] {message}"

    def __len__(self):                               # len(logger) のとき
        return len(self.log)

    def __bool__(self):                              # if logger: のとき
        return len(self.log) > 0

app = Logger("app")
print(app("起動しました"))   # [app] 起動しました
print(app("ログイン成功"))   # [app] ログイン成功
print(len(app))               # 2
if app:
    print("ログがあります")    # ログがあります
Logger に組み込み操作を教える
app('msg')app.__call__('msg')len(app)app.__len__()if app:app.__bool__()変換変換変換
logger(...) / len(logger) / if logger: のような 書き慣れた構文 を、自作クラスに 3 種類のダンダーで教え込んでいる。

__bool__ を書かない場合の挙動

__bool__ を定義していない場合、Python は次に __len__ を見にいきます。__len__ の戻り値が 0 なら False、それ以外なら True と扱われます。両方とも未定義のクラスは、インスタンスがどんな状態でも常に True 扱いになります。空のリストや空文字列が False と評価されるのもこの仕組みです。

Logger クラスに 3 つの特殊メソッドを実装して、ログの追加・件数取得・件数判定を組み込み構文だけで書けるようにします。

class Logger: を定義し、__init__(self, name)self.name = name / self.log = [] を代入してください。

__call__(self, message) を定義し、self.log.append(message) を実行してから return f"[{self.name}] {message}" を返してください。

__len__(self) を定義し、return len(self.log) を返してください。

__bool__(self) を定義し、return len(self.log) > 0 を返してください。

error_log = Logger("error") を作り、print(bool(error_log))error_log("DBエラー")print(len(error_log))print(bool(error_log)) の順に実行してください。

Python エディタ

コードを実行してください

他にも __hash__setdict のキーにする)、__lt__ / __gt__< / > の比較)、__getitem__obj[key] の添字アクセス)、__iter__for x in obj: での反復)など、特殊メソッドはたくさんあります。すべてを丸暗記する必要はなく、「組み込みの操作を自作クラスに教えたいときに、対応するダンダーがある」 ということを理解すれば、そのつど調べて使えるようになります。

特殊メソッド書く構文呼ばれるタイミング
__init__Money(300)インスタンス生成時
__add__a + b+ 演算子
__str__print(p) / str(p)ユーザー向け文字列化
__repr__repr(p) / 対話シェル表示開発者向け文字列化
__eq__a == b等価比較
__call__obj(...)関数のような呼び出し
__len__len(obj)長さ取得
__bool__if obj: / bool(obj)真偽値評価
QUIZ

理解度チェック

まずは1問ずつ答えてみましょう。

Q1次のコード result = a + b を実行したとき、Python が 裏で呼び出すメソッド はどれですか?

Q2__str____repr__ の役割の説明として、最も適切 なものはどれですか?

Q3__eq__定義していない クラスで a == b を実行すると、何が比較されますか?(ab は同じクラスのインスタンス)