目次
15 章

マジックメソッドの深さとプロトコル

Pythonオブジェクトが言語機能とつながるすべてのフック。__call__、__getitem__、__hash__、__format__、__getattr__ などをまとめます。

第3部 深さ・並行性 の最初の章です。第2部を経て @dataclass、デコレータ、コンテキストマネージャといった「道具を使う人」の語彙が固まったなら、第3部からは その道具たちがどうやって作られているのか の視点に入ります。

本章のマジックメソッドは、本書全体で最もよく出会います。第8章 dataclass が自動で作ってくれていた __init__ / __repr__ / __eq__、第9章 Protocol、第10章 コンテキストマネージャ__enter__ / __exit__、第11章 イテラブル、ジェネレータ__iter__ / __next__ — すべてマジックメソッドです。本章ではそのフックを整理して見渡します。

マジックメソッド (またはダンダーメソッド、dunder = double underscore) は、Pythonオブジェクトが言語機能と出会う 公式フック です。len(x)x.__len__() を呼び、a + ba.__add__(b) を呼ぶ、という具合です。このフックを正確に知れば Python らしいオブジェクトを作れて、ライブラリコードを読むときに何が呼ばれているかも見えてきます。

オブジェクトのライフサイクル #

__init____new__ — 違い #

2 つの役割
class Foo:
    def __new__(cls, *args, **kwargs):
        # インスタンスを作る (メモリ確保)
        instance = super().__new__(cls)
        return instance

    def __init__(self, value):
        # 作られたインスタンスを初期化する
        self.value = value
  • __new__ — クラスメソッドのように動作し、インスタンスを作って返しますcls を最初の引数に受け取ります
  • __init__ — インスタンスメソッド、すでに作られたインスタンスを初期化しますself を受け取ります

ほとんどのコードは __init__ だけを書きます。__new__ が必要な場面は限られます。

  • 不変型 (tuplestr) を継承して作る
  • シングルトンのようなインスタンスキャッシュ
  • __init__ 呼び出し自体を防ぎたいとき
__new__ が他のオブジェクトを返すと
class Singleton:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

a = Singleton()
b = Singleton()
print(a is b)   # True

__new__ が返したオブジェクトが そのクラスのインスタンスでなければ __init__ は呼ばれません。微妙な部分なので注意。

__del__ — ほとんど使わない #

オブジェクトがガベージコレクションされるときに呼ばれます。ただし。

  • いつ呼ばれるか保証されない — GC のタイミングに依存
  • 循環参照があると呼ばれないこともある
  • 例外が出ても無視される

リソースの後片付けは __del__ ではなく コンテキストマネージャ (第10章) を使う方が安全です。

表現 — __repr__ vs __str__ #

repr / str
class Point:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __repr__(self) -> str:
        return f"Point(x={self.x}, y={self.y})"

    def __str__(self) -> str:
        return f"({self.x}, {self.y})"

p = Point(1, 2)
print(repr(p))   # Point(x=1, y=2)   ← デバッグ用、曖昧でなく
print(str(p))    # (1, 2)            ← 人が読みやすく
print(f"{p}")    # (1, 2)            ← f-string は str
print(p)         # (1, 2)            ← print も str

ルール。

  • __repr__曖昧でない表現。可能なら eval(repr(x)) == x になるように
  • __str__ユーザーに見せる表現。定義しないと __repr__ が使われる

@dataclass (第8章) が自動で作るのは __repr__ です。

__format__ — f-string のフォーマット指定 #

フォーマット指定を受け取る
class Money:
    def __init__(self, amount, currency):
        self.amount, self.currency = amount, currency

    def __format__(self, spec):
        if spec == "k":
            return f"{self.amount / 1000:.1f}k {self.currency}"
        return f"{self.amount} {self.currency}"

m = Money(12345, "KRW")
print(f"{m}")     # 12345 KRW
print(f"{m:k}")   # 12.3k KRW

f-string f"{x:fmt}"fmt 部分が __format__(spec) の引数として渡されます。ドメインオブジェクトにユーザー定義のフォーマットを与えるフックです。

比較 #

__eq____hash__ — 相棒 #

この 2 つは 必ずセットで動きます。

基本ルール
class User:
    def __init__(self, id, name):
        self.id, self.name = id, name

    def __eq__(self, other):
        if not isinstance(other, User):
            return NotImplemented
        return self.id == other.id

    def __hash__(self):
        return hash(self.id)

ルール。

  • a == b なら hash(a) == hash(b)必ず 成立しなければならない
  • __eq__ だけ定義すると __hash__ が自動で None になり、set / dict のキーに使えない
  • 可変オブジェクトは通常 hashable ではない

@dataclass(frozen=True) が両メソッドを一緒に自動生成します。

__lt__ などの比較 — functools.total_ordering #

<<=>>===!= の 6 つを全部書くのが面倒なとき。

total_ordering
from functools import total_ordering

@total_ordering
class Score:
    def __init__(self, value):
        self.value = value

    def __eq__(self, other):
        return self.value == other.value

    def __lt__(self, other):
        return self.value < other.value
# 残りの 4 つは自動で埋まる

@dataclass(order=True) の方が短いですが、単純なフィールド比較 にしか向いていません。複雑な比較ロジックは total_ordering で。

コンテナのように — シーケンス / マッピング #

__len____getitem____contains____iter__ #

シーケンスを作る
class Page:
    def __init__(self, items):
        self.items = items

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

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

    def __contains__(self, value):
        return value in self.items

    def __iter__(self):
        return iter(self.items)

p = Page(["a", "b", "c"])
len(p)            # 3
p[0]              # 'a'
"b" in p          # True
list(p)           # ['a', 'b', 'c']
for x in p: ...   # OK

この 4 つを埋めれば ほぼ list のように 動作します。実は __getitem____len__ だけでも for in が動作します (インデックス 0 から IndexError まで試行)。

スライスも自動 #

__getitem__ の引数に slice オブジェクト が渡されることがあります。

スライスの処理
class MyList:
    def __init__(self, items):
        self.items = items

    def __getitem__(self, key):
        if isinstance(key, slice):
            return MyList(self.items[key])
        return self.items[key]

m = MyList([1, 2, 3, 4, 5])
m[1:3].items   # [2, 3]

m[1:3] の呼び出し時、keyslice(1, 3, None) です。

__setitem____delitem__ #

書き込み / 削除
class Cache:
    def __init__(self):
        self._data = {}

    def __getitem__(self, key):
        return self._data[key]

    def __setitem__(self, key, value):
        self._data[key] = value

    def __delitem__(self, key):
        del self._data[key]

c = Cache()
c["x"] = 1
print(c["x"])   # 1
del c["x"]

dict のように動作するオブジェクトを作るときに使います。

呼び出し可能 — __call__ #

オブジェクトを関数のように呼び出せるようにするフック。

__call__
class Counter:
    def __init__(self):
        self.count = 0

    def __call__(self, value):
        self.count += 1
        return value * 2

c = Counter()
print(c(5))        # 10
print(c(7))        # 14
print(c.count)     # 2

第12章 デコレータパターンクラス形式のデコレータ が本フックの上に作られています。PyTorch の nn.Module__call__ を持ち、モデルを関数のように呼び出せます。

属性アクセス — __getattr____setattr____getattribute__ #

__getattr__ — 存在しない属性を要求されたとき #

存在しない属性の lazy 処理
class Lazy:
    def __getattr__(self, name):
        if name.startswith("get_"):
            field = name[4:]
            return lambda: f"value of {field}"
        raise AttributeError(name)

l = Lazy()
l.get_name()   # 'value of name'
l.get_age()    # 'value of age'

存在しない属性 のときだけ呼ばれます。存在する属性は通常の経路を通ります。ORM の自動メソッド、プロキシオブジェクトなどでよく登場します。

__getattribute__ — すべての属性要求に対して #

すべての属性をフック — 危険
class All:
    def __getattribute__(self, name):
        print(f"アクセス: {name}")
        return super().__getattribute__(name)

すべての属性アクセスに割り込みます。 誤って使うと無限再帰が起こりやすいため (自分の中で self.x を使うとまた __getattribute__ が呼ばれる)、通常は __getattr__ だけを使います。

__setattr____delattr__ #

書き込みフック
class Frozen:
    def __init__(self, x):
        self.x = x
        object.__setattr__(self, "_locked", True)

    def __setattr__(self, name, value):
        if getattr(self, "_locked", False):
            raise AttributeError(f"{name} は変更不可")
        super().__setattr__(name, value)

f = Frozen(5)
f.x = 10   # AttributeError

@dataclass(frozen=True) の動作がまさに本パターンです。

算術演算 — __add__ など #

演算子オーバーロード
class Vec:
    def __init__(self, x, y):
        self.x, self.y = x, y

    def __add__(self, other):
        return Vec(self.x + other.x, self.y + other.y)

    def __mul__(self, k):
        return Vec(self.x * k, self.y * k)

    def __rmul__(self, k):
        return self.__mul__(k)

v = Vec(1, 2) + Vec(3, 4)        # __add__
w = Vec(1, 2) * 3                 # __mul__
u = 3 * Vec(1, 2)                 # __rmul__ (左側のオペランドが知らない型)

対称な演算が必要なら __rmul__ のような reflected バージョンも定義します。左側のオペランドが自分を掛ける方法を知らないとき (例: int * Vec) に呼ばれます。

__bool__ — 真理値 #

bool
class Bag:
    def __init__(self):
        self.items = []

    def __bool__(self):
        return bool(self.items)

b = Bag()
if b:
    print("何かある")

定義しないと __len__ を見て、それもなければ常に True。コンテナ形式には __len__ だけで十分な場面が多いです。

継承時のフック — __init_subclass__ #

サブクラスが 作られるとき に呼ばれるフック。メタクラスを使わなくても似たことができます。

__init_subclass__
class Plugin:
    registry = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        Plugin.registry.append(cls)

class JsonPlugin(Plugin):
    pass

class CsvPlugin(Plugin):
    pass

print(Plugin.registry)
# [<class 'JsonPlugin'>, <class 'CsvPlugin'>]

プラグインの自動登録 などの場面でよく使われます。メタクラス (第17章 メタクラス) の方が強力ですが、この程度の仕事なら __init_subclass__ の方が軽くて安全です。

よく出会うメソッド — 一覧表 #

カテゴリメソッド呼ばれるタイミング
生成 / 破棄__new____init____del__インスタンス作成時 / 後片付け時
表現__repr____str____format____bool__repr()str()f""if
比較__eq____hash____lt__ など==hash()<
コンテナ__len____getitem____setitem____contains____iter__len()x[k]infor
呼び出し__call__x(...)
属性__getattr____setattr____delattr__属性アクセス
算術__add____sub____mul____truediv__、…+-*/
非同期__await____aiter____anext____aenter____aexit__awaitasync for / with
継承フック__init_subclass____class_getitem__サブクラス / Cls[T]

本表を全部覚える必要はありません。どんなフックがある という事実だけ把握しておけば、ライブラリコードで見かけたときに検索できます。

練習問題 #

  1. Money クラスを作ってください。フィールドは amount: intcurrency: str__repr__Money(amount=1234, currency='KRW')__str__1,234 KRW の形式。__format__f"{m:k}"1.2k KRW のように千単位で表示されるようにします。
  2. Page クラスを作ってください。__len__ / __getitem__ (スライス処理を含む) / __contains__ / __iter__ を実装して list のように動作させます。Page([1,2,3,4,5])[1:3] が新しい Page([2,3]) を返すか確認します。
  3. __init_subclass__ で自動プラグイン登録器を作ってください。class Plugin: のサブクラスが作られると、クラス変数 registry: list[type] に自動で追加されます。サブクラスを 2 つ定義したあと Plugin.registry を出力して確認します。

一行まとめ: マジックメソッドはオブジェクトが言語機能と出会う公式フック。__new__ (生成) vs __init__ (初期化)、__repr__ (開発者) vs __str__ (ユーザー)、__eq____hash__ は相棒、コンテナは __len__ + __getitem__、関数のように振る舞うには __call__、属性は __getattr__ (存在しないものだけ) / __getattribute__ (すべて、危険)、サブクラスの自動登録は __init_subclass__

次の章 #

次の 第16章 ディスクリプタと __set_name__ では、マジックメソッドの中でも特殊な分類 — 属性をオブジェクト化するディスクリプタ を扱います。@property は実はディスクリプタの一形態です。

X