紛失届
LOST-008
届出人
展示会の輸送担当、ナムさん
なくしたもの
展示品の箱のクラス Crate。箱をコンベヤーに載せると len() も for も + もエラーになり、print() はわけのわからないアドレスしか表示しない。
解決の条件
Crate に空港の標準手順(ダンダーメソッド)を付けて、組み込み関数や演算子が箱を見分けられるようにする。
ハヌルソム国際空港 手荷物センター
ナムさんは、展示品の箱を自作のクラスで管理しています。箱そのものは問題ないのに、コンベヤーに載せたとたんに止まってしまうそうです。ド・ミラン班長が箱をぐるりと回してみます。「コンベヤーは箱の形なんか見ない。取っ手をつかむんだ。この箱には取っ手が一つもないね」
Pythonの文法や組み込み関数は、オブジェクトの決まった名前のメソッドをつかんで動きます。名前の前後にアンダースコアが二つずつ付くので、「ダンダー(double underscore)メソッド」と呼ばれます。今日はその取っ手を一つずつ付けていきます。
届出人の箱はこうです。同じ箱に四つのことをやらせてみます。
class Crate:
def __init__(self, items):
self.items = list(items)
c = Crate(["vase", "lamp"])
for expr in ["len(c)", "c[0]", "c + c", "iter(c)"]:
try:
eval(expr)
except TypeError as e:
print(f"{expr:7} → {e}")
# len(c) → object of type 'Crate' has no len()
# c[0] → 'Crate' object is not subscriptable
# c + c → unsupported operand type(s) for +: 'Crate' and 'Crate'
# iter(c) → 'Crate' object is not iterablelen(c) は魔法ではありません。Pythonは type(c).__len__(c) を呼びます。そのメソッドがないので、「len がない」というエラーになります。__len__ を付ければ、すぐに動きます。
class Crate:
def __init__(self, items):
self.items = list(items)
def __len__(self):
return len(self.items)
c = Crate(["vase", "lamp"])
print(len(c), type(c).__len__(c))
# 2 2
c.__len__ = lambda: 99
print(len(c), c.__len__())
# 2 99最後の2行が大事です。インスタンスに __len__ を付けても、len(c) は2です。文法が呼ぶダンダーメソッドは、インスタンスではなく型(クラス)で探すからです。コンベヤーが従うのは、規格書に書かれた手順だけです。
| 文法 | 実際に呼ばれるもの |
|---|---|
len(x) | type(x).__len__(x) |
x[i]、x[i] = v | __getitem__、__setitem__ |
a + b | a.__add__(b)、だめなら b.__radd__(a) |
a == b | __eq__ |
v in x | __contains__(なければ繰り返しながら比較) |
for v in x | __iter__(なければ __getitem__) |
bool(x)、if x: | __bool__(なければ __len__) |
print(x)、str(x) | __str__(なければ __repr__) |
x() | __call__ |
with x: | __enter__、__exit__ |
こうした取っ手をうまく付けたクラスは、よく「Pythonらしいクラス」と呼ばれます。size() や merge() といったメソッドを別に覚えなくても、もう知っている len や + で扱えるからです。
下のダンダー交換台は、取っ手を付けた Crate に文法を1行入れると、Pythonが実際に呼んだメソッドを順番に見せてくれます。
ダンダー交換台
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __repr__(self):
5
return f"Crate({self.items})"
6
def __len__(self):
7
return len(self.items)
8
def __getitem__(self, i):
9
return self.items[i]
10
def __add__(self, other):
11
return Crate(self.items + other.items)
12
13
c = Crate(["vase", "lamp"])
14
d = Crate(["cup"])
bool(c) は __bool__ がないので、代わりに __len__ に尋ねます。長さが0でなければ真です。for x in c で __getitem__ が呼ばれる理由は、8-3で見ます。ピンの案内
→ ダンダーメソッドは付けておくだけにして、呼ぶときは c.__len__() ではなく len(c) のように文法で呼びましょう。文法のほうが型で探してくれるうえ、だめなときの別の道(__radd__ に回す、__bool__ の代わりに __len__ に尋ねる)まで面倒を見てくれます。
__repr__ と __str__ (7分)届出人がデバッグのために箱を表示すると、こんな文字しか出てきませんでした。
class Crate:
def __init__(self, items):
self.items = list(items)
print(Crate(["vase"]))
# 例:<__main__.Crate object at 0x000001F4C2A8B2C0>object から受け継いだデフォルトの __repr__ は、型の名前とアドレスだけです。箱の中に何が入っているのかわかりません。文字に変える取っ手は二つあります。
| 誰のための文字か | 使われる場所 | |
|---|---|---|
__repr__ | 開発者。できれば Crate([...]) のように作り直せる形 | repr()、対話シェル、デバッガー、リスト・dict の中の要素、f-string の !r |
__str__ | 利用者。読みやすい文 | print()、str()、f-string の {x} |
class Crate:
def __init__(self, items):
self.items = list(items)
def __repr__(self):
return f"Crate({self.items})"
def __str__(self):
return f"crate of {len(self.items)}"
c = Crate(["vase", "lamp"])
print(c)
# crate of 2
print(repr(c))
# Crate(['vase', 'lamp'])
print([c])
# [Crate(['vase', 'lamp'])]
print(f"{c} / {c!r}")
# crate of 2 / Crate(['vase', 'lamp'])print([c]) では、__str__ ではなく __repr__ が使われました。リストを文字にするとき、要素はいつでも repr で書かれます。__str__ がなければ、str() と print() は __repr__ を使います。逆はありません。だから一つだけ付けるなら、まず __repr__ です。repr です。バグを追うときに見える姿が、そのまま __repr__ なのです。ダンダー交換台
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __repr__(self):
5
return f"Crate({self.items})"
6
def __str__(self):
7
return f"crate of {len(self.items)}"
8
9
c = Crate(["vase", "lamp"])
箱がコンテナのように振る舞うには、五つの取っ手で十分です。__len__、__getitem__、__iter__、__contains__、__bool__ です。ところが8-1では、__iter__ がないのに for が回りました。
class OldCrate:
def __init__(self, items):
self.items = list(items)
def __getitem__(self, i):
print("getitem", i)
return self.items[i]
for x in OldCrate(["vase", "lamp"]):
pass
# getitem 0
# getitem 1
# getitem 2
print("lamp" in OldCrate(["vase", "lamp"]))
# getitem 0
# getitem 1
# True__iter__ がないと、iter() は __getitem__ を0、1、2…の順に呼ぶイテレーターを作ります。IndexError が出たら「終わり」と判断して、静かに止まります。だから getitem 2 まで表示され、エラーなしで終わったのです。__contains__ がないと、in はこうして一つずつ取り出して == で比べます。見つかればそこで止まります。
この道は古い互換用です。新しく作るクラスには、取っ手をきちんと付けましょう。
class Crate:
def __init__(self, items):
self.items = list(items)
def __len__(self):
return len(self.items)
def __iter__(self):
yield from self.items
def __contains__(self, x):
return x in self.items
c, e = Crate(["vase", "lamp"]), Crate([])
print(list(c), "lamp" in c, bool(c), bool(e))
# ['vase', 'lamp'] True True False
if not e:
print("empty crate")
# empty crate__iter__ はイテレーターを返さなければなりません。return iter(self.items) でもよいですし、上のように yield を使えば関数がジェネレーターになり、イテレーターを自動で作ってくれます。ジェネレーターについては、ジェネレーター・デコレーターの講座でさらに詳しく扱います。__contains__ があれば、in は繰り返さずにそのメソッドに直接尋ねます。__bool__ がなければ、__len__ が0かどうかで真偽を決めます。どちらもなければ、オブジェクトはいつでも真です。だから空の Crate も、__len__ さえ付ければ if not e: が自然に書けます。ダンダー交換台
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __len__(self):
5
return len(self.items)
6
def __iter__(self):
7
yield from self.items
8
def __contains__(self, x):
9
return x in self.items
10
11
c = Crate(["vase", "lamp"])
12
e = Crate([])
__add__、__radd__、NotImplemented (8分)a + b は次のように処理されます。
type(a).__add__(a, b) を呼びます。NotImplemented なら、「この組み合わせは知らない」という意味です。Pythonは相手側の type(b).__radd__(b, a) に回します。NotImplemented なら TypeError です。class Crate:
def __init__(self, items):
self.items = list(items)
def __repr__(self):
return f"Crate({self.items})"
def __add__(self, other):
if not isinstance(other, Crate):
return NotImplemented
return Crate(self.items + other.items)
def __radd__(self, other):
if other == 0:
return self
return NotImplemented
c, d = Crate(["vase"]), Crate(["cup"])
print(c + d)
# Crate(['vase', 'cup'])
print(sum([c, d]))
# Crate(['vase', 'cup'])
c + 1
# TypeError: unsupported operand type(s) for +: 'Crate' and 'int'sum() は0から始めて、0 + c から計算します。int.__add__(0, c) は箱を知らないので NotImplemented を返し、そこで Crate.__radd__(c, 0) が呼ばれます。__radd__ が0を受け止めてくれるので、sum が動きます。NotImplemented を返します。そうすれば相手側に出番が回ります。NotImplementedError(例外)とは別物です。ダンダー交換台
1
class Crate:
2
def __init__(self, items):
3
self.items = list(items)
4
def __repr__(self):
5
return f"Crate({self.items})"
6
def __add__(self, other):
7
if not isinstance(other, Crate):
8
return NotImplemented
9
return Crate(self.items + other.items)
10
def __radd__(self, other):
11
if other == 0:
12
return self
13
return NotImplemented
14
15
c = Crate(["vase"])
16
d = Crate(["cup"])
2限目で見た += の二つの顔の謎が、ここで解けます。a += b はまず __iadd__(インプレース加算)を探し、なければ a = a + b に置き換えて、新しいオブジェクトにタグを付け替えます。list には __iadd__ があり、tuple と str にはありません。
c = Crate(["vase"])
alias = c
c += Crate(["cup"])
print(c is alias, alias)
# False Crate(['vase'])
class Crate2(Crate):
def __iadd__(self, other):
self.items += other.items
return self
c = Crate2(["vase"])
alias = c
c += Crate2(["cup"])
print(c is alias, alias)
# True Crate(['vase', 'cup'])__iadd__ があれば、かばんの中を変えて自分自身を返します。Pythonはその戻り値に c のタグを付け直しますが、同じかばんなのでタグはそのままです。(class Crate2(Crate) は Crate を受け継ぐ規格書です。9限目のテーマです。)
ピンの案内
→ __iadd__ で return self を忘れると、c += d のあと c が None になります。Pythonは __iadd__ が返したものにタグを付け直すからです。
__call__、with、@dataclass (7分)__call__ を付けると、オブジェクトを関数のように呼べます。設定値を持ち歩く関数が必要なときに使います(6限目のクロージャと同じことをクラスで)。
class Scale:
def __init__(self, limit):
self.limit = limit
def __call__(self, kg):
return kg <= self.limit
check = Scale(23)
print(check(20), check(30), callable(check))
# True False Truewith は __enter__ と __exit__ を呼びます。ブロックの中で例外が起きても、__exit__ は必ず呼ばれます。リソースの後始末に使われる理由で、11限目でまた出会います。
class Belt:
def __enter__(self):
print("belt on")
return self
def __exit__(self, exc_type, exc, tb):
print("belt off", exc_type)
return False
with Belt() as b:
print("moving")
# belt on
# moving
# belt off None__exit__ が True を返すと、例外を握りつぶします。ほとんどの場合は False(または何も返さない)が正解です。
__init__、__repr__、__eq__ のように、いつも似たように書く取っ手は、@dataclass が作ってくれます。
from dataclasses import dataclass
@dataclass
class Tag:
code: str
kg: int = 0
t = Tag("ICN", 12)
print(t, t == Tag("ICN", 12))
# Tag(code='ICN', kg=12) True
print(Tag.__hash__)
# None最後の行を見てください。__eq__ を定義すると、Pythonは __hash__ を None にします。自分で書いたクラスでも同じです。
class Crate:
def __init__(self, items):
self.items = list(items)
def __eq__(self, other):
return isinstance(other, Crate) and self.items == other.items
print(Crate(["vase"]) == Crate(["vase"]), Crate.__hash__)
# True None
{Crate(["vase"])}
# TypeError: cannot use 'Crate' as a set element (unhashable type: 'Crate')「値が同じなら等しい」と決めたのに、ハッシュが相変わらず同一性(アドレス)基準のままだと、等しい二つの箱が set の別々の枠に入ってしまいます。その約束を破らないように、ハッシュを最初から無効にしておくのです。ハッシュと等価性の約束は、12限目で詳しく見ます。
取っ手をすべて付けた Crate です。
class Crate:
def __init__(self, items=()):
self.items = list(items)
def __repr__(self):
return f"Crate({self.items})"
def __len__(self):
return len(self.items)
def __getitem__(self, i):
return self.items[i]
def __iter__(self):
return iter(self.items)
def __contains__(self, x):
return x in self.items
def __add__(self, other):
if not isinstance(other, Crate):
return NotImplemented
return Crate(self.items + other.items)
def __radd__(self, other):
return self if other == 0 else NotImplemented
a, b = Crate(["vase", "lamp"]), Crate(["frame"])
print(len(a), a[0], "lamp" in a, [x.upper() for x in a])
# 2 vase True ['VASE', 'LAMP']
print(a + b, sum([a, b]))
# Crate(['vase', 'lamp', 'frame']) Crate(['vase', 'lamp', 'frame'])
print(a[-1], a[:1])
# lamp ['vase']a[:1] では __getitem__ にスライスオブジェクトが渡され、リストのスライスがそのまま返ってきました。箱として返したいなら、isinstance(i, slice) で分けて処理すればよいのです。
ナムさんの箱が、コンベヤーの上を静かに流れていきます。班長が言います。「形はそのまま。取っ手を付けただけさ」
解決報告 — LOST-008。Crate に取っ手(ダンダーメソッド)がなかったため、文法が箱を見分けられなかった。repr・len・getitem・iter・contains・add・radd を付けて解決。
Crate に __eq__ を足して、{Crate(["vase"])} がどうなるか確かめてください。なぜそうなるのか、一文で書いてみましょう。__bool__ がいつも False を返すクラスを作り、__len__ が3のとき if obj: がどちらに従うかを予想してから確かめてください。__getitem__ を直して、isinstance(i, slice) なら Crate を返すようにし、a[:1] が Crate(['vase']) になるようにしてください。班長の業務手帳
文法はダンダーメソッドを呼ぶ。len(x) は type(x).__len__(x)。インスタンスではなく型で探す。
一つだけ付けるなら __repr__。print は __str__ がなければ __repr__ を使い、リストの中の要素はいつも repr。
for は __iter__、なければ __getitem__ を0から(IndexError で止まる)。bool は __bool__、なければ __len__。
知らない組み合わせには NotImplemented を返す。すると相手側の __radd__ に出番が回る。
a += b は __iadd__ があればその場で(return self)、なければ a = a + b。
__eq__ を定義すると __hash__ は None。@dataclass も同じ。
新しい授業・講座のお知らせを受け取る
この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。
R
ReflexKR
触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。
ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する
アフィリエイトと支援について© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
質問と回答
この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。