R

ReflexKR

Reflex公式ドキュメント
名札の空港:Pythonのオブジェクトモデル
8限目

時間 5:50–6:40

空港の標準手順:ダンダーメソッド

紛失届

LOST-008

コンベヤーが見分けられない箱

届出人

展示会の輸送担当、ナムさん

なくしたもの

展示品の箱のクラス Crate。箱をコンベヤーに載せると len() も for も + もエラーになり、print() はわけのわからないアドレスしか表示しない。

解決の条件

Crate に空港の標準手順(ダンダーメソッド)を付けて、組み込み関数や演算子が箱を見分けられるようにする。

ハヌルソム国際空港 手荷物センター

ナムさんは、展示品の箱を自作のクラスで管理しています。箱そのものは問題ないのに、コンベヤーに載せたとたんに止まってしまうそうです。ド・ミラン班長が箱をぐるりと回してみます。「コンベヤーは箱の形なんか見ない。取っ手をつかむんだ。この箱には取っ手が一つもないね」

Pythonの文法や組み込み関数は、オブジェクトの決まった名前のメソッドをつかんで動きます。名前の前後にアンダースコアが二つずつ付くので、「ダンダー(double underscore)メソッド」と呼ばれます。今日はその取っ手を一つずつ付けていきます。

8-1. 文法はダンダーメソッドを呼ぶ (9分)

届出人の箱はこうです。同じ箱に四つのことをやらせてみます。

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 iterable

len(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 + ba.__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"])

c + d は __add__ の中で新しい箱を作るので、__init__ が先に終わります。print(c) は __str__ がないので __repr__ を呼びます。for は __iter__ がないので __getitem__ を0、1の順に呼び、2で IndexError が出ると止まります(一覧の最後の行の raise IndexError)。
  • bool(c) は __bool__ がないので、代わりに __len__ に尋ねます。長さが0でなければ真です。
  • for x in c で __getitem__ が呼ばれる理由は、8-3で見ます。

ピンの案内

→ ダンダーメソッドは付けておくだけにして、呼ぶときは c.__len__() ではなく len(c) のように文法で呼びましょう。文法のほうが型で探してくれるうえ、だめなときの別の道(__radd__ に回す、__bool__ の代わりに __len__ に尋ねる)まで面倒を見てくれます。

8-2. __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"])

print(c) と f'{c}' は __str__ を、repr(c) と f'{c!r}' は __repr__ を呼びます。print([c]) はリストの中の要素なので __repr__ を呼びます。

8-3. コンテナのプロトコル (9分)

箱がコンテナのように振る舞うには、五つの取っ手で十分です。__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([])

in は __contains__ に、for と max は __iter__ にまっすぐ向かいます。list(c) は前もって場所を確保するために __len__ にも尋ねます(CPython)。bool(e) は __bool__ がないので __len__ を呼びます。

8-4. 演算子:__add__、__radd__、NotImplemented (8分)

a + b は次のように処理されます。

  1. type(a).__add__(a, b) を呼びます。
  2. その結果が NotImplemented なら、「この組み合わせは知らない」という意味です。Pythonは相手側の type(b).__radd__(b, a) に回します。
  3. どちらも 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"])

0 + c は、int が箱を知らないので Crate.__radd__ に回ります。c + 1 は __add__ が NotImplemented を返し、int も知らないので TypeError になります。c += d は __iadd__ がないので、__add__ で新しい箱を作ります。

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__ が返したものにタグを付け直すからです。

8-5. __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 True

with は __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限目で詳しく見ます。

事件解決 (10分)

取っ手をすべて付けた 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 を付けて解決。

練習問題

  1. 事件解決の Crate に __eq__ を足して、{Crate(["vase"])} がどうなるか確かめてください。なぜそうなるのか、一文で書いてみましょう。
  2. __bool__ がいつも False を返すクラスを作り、__len__ が3のとき if obj: がどちらに従うかを予想してから確かめてください。
  3. __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 も同じ。

← 前へ

7限目 · かばんの規格書:クラスとインスタンス

次へ →

9限目 · 規格書の規格書:継承とMRO

新しい授業・講座のお知らせを受け取る

この講座に授業が追加されたり、続きの講座が公開されたりしたらメールでお知らせします。

収集項目:メールアドレス・目的:この講座の新しい授業・公開のお知らせ・保有期間:解除のご依頼まで。他の目的には使わず、第三者にも提供しません。お知らせメールに返信いただければすぐに削除します。

質問と回答

この回について気になることを書き込んでください。すべての言語のページの質問がここに集まります。

リンクや書式なしで、そのままの文字で表示されます。

R

ReflexKR

触って学ぶPythonとReflex。すべての例はReflex 0.10.0で実際に動かして確認しています。

ReflexKRはバナー広告なし・無料で運営しています。役に立ったら、コーヒー一杯で応援してください。 ☕ コーヒーで応援する

アフィリエイトと支援について
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex