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

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

Pythonの特殊メソッド(ダンダー)をMoney/User/Couponで解説。__add__で +、__str__と__repr__の使い分け、__eq__で == の意味を定義、__call__/__len__/__bool__の用途を確認できます。

前回まででインスタンスメソッド・クラスメソッド・スタティックメソッドの 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は同じクラスのインスタンス)