マジックメソッドの深さとプロトコル
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 + b が a.__add__(b) を呼ぶ、という具合です。このフックを正確に知れば Python らしいオブジェクトを作れて、ライブラリコードを読むときに何が呼ばれているかも見えてきます。
オブジェクトのライフサイクル #
__init__ と __new__ — 違い
#
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__ が必要な場面は限られます。
- 不変型 (
tuple、str) を継承して作る - シングルトンのようなインスタンスキャッシュ
__init__呼び出し自体を防ぎたいとき
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__
#
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 KRWf-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 つを全部書くのが面倒なとき。
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] の呼び出し時、key は slice(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__
#
オブジェクトを関数のように呼び出せるようにするフック。
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__ — 存在しない属性を要求されたとき
#
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__ — 真理値
#
class Bag:
def __init__(self):
self.items = []
def __bool__(self):
return bool(self.items)
b = Bag()
if b:
print("何かある")定義しないと __len__ を見て、それもなければ常に True。コンテナ形式には __len__ だけで十分な場面が多いです。
継承時のフック — __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]、in、for |
| 呼び出し | __call__ | x(...) |
| 属性 | __getattr__、__setattr__、__delattr__ | 属性アクセス |
| 算術 | __add__、__sub__、__mul__、__truediv__、… | +、-、*、/ |
| 非同期 | __await__、__aiter__、__anext__、__aenter__、__aexit__ | await、async for / with |
| 継承フック | __init_subclass__、__class_getitem__ | サブクラス / Cls[T] |
本表を全部覚える必要はありません。どんなフックがある という事実だけ把握しておけば、ライブラリコードで見かけたときに検索できます。
練習問題 #
Moneyクラスを作ってください。フィールドはamount: int、currency: str。__repr__はMoney(amount=1234, currency='KRW')、__str__は1,234 KRWの形式。__format__でf"{m:k}"が1.2k KRWのように千単位で表示されるようにします。Pageクラスを作ってください。__len__/__getitem__(スライス処理を含む) /__contains__/__iter__を実装して list のように動作させます。Page([1,2,3,4,5])[1:3]が新しいPage([2,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 は実はディスクリプタの一形態です。