R

ReflexKR

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

時間 6:40–7:30

規格書の規格書:継承とMRO

紛失届

LOST-009

二つの規定のうち一つが消えた

届出人

航空貨物の規定を担当する開発者、ハン課長

なくしたもの

モバイルバッテリーの規格 PowerBank の危険物検査。「機内持ち込み」規定と「危険物」規定の両方を継承させたのに、検査結果に危険物の項目がない。

解決の条件

PowerBank が規定を読む順番(MRO)を明らかにし、二つの規定が両方とも実行されるように直す。

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

ハン課長は、モバイルバッテリーの規格を新しく作りました。機内持ち込みの規格書と危険物の規格書を両方とも継承させたので、検査も両方行われるはずだと思っていました。ところが検査表には大きさの検査しかなく、危険物の検査がありません。

ド・ミラン班長が二枚の規格書を重ねてみます。「上位の規格書が二つあるなら、どちらを先に読むかが決まっている。まずその順番から見よう」

9-1. 上位の規格書:継承とオーバーライド (7分)

class CarryOn(Item): は、「CarryOn にないものは Item で探す」という意味です。この探す順番が、クラスの __mro__(Method Resolution Order)に書かれています。

class Item:
    kind = "item"
    def label(self):
        return f"[{self.kind}]"

class CarryOn(Item):
    kind = "carry-on"

b = CarryOn()
print(b.label())
# [carry-on]
print(CarryOn.__mro__)
# (<class '__main__.CarryOn'>, <class '__main__.Item'>, <class 'object'>)
print("label" in vars(CarryOn), "label" in vars(Item))
# False True

b.label() を読む過程は7限目と同じです。インスタンスの __dict__ にないので、__mro__ を前から順に見ていきます。CarryOn には label がなく、Item で見つかります。ところが label の中の self.kind は、またインスタンスから探すので、CarryOn.kind の "carry-on" が出てきます。

オーバーライドは特別な文法ではありません。子の規格書に同じ名前を書けば、MROで先に見つかったものが勝つだけです。親のものを引き継いで使いたければ、super() で呼びます。

class Fragile(Item):
    kind = "fragile"
    def label(self):
        return super().label() + "!"

print(Fragile().label())
# [fragile]!

すべてのクラスのMROは object で終わります。__repr__ や __eq__ のような基本の取っ手は、どれもそこから来ています(8限目)。

9-2. 多重継承とダイヤモンド (8分)

届出人の規格は、二つの規格書を継承しています。どちらも Item を継承しているので、図に描くとひし形(ダイヤモンド)になります。

class Item:
    rule = "item"
    tag = "plain"
class CarryOn(Item):
    rule = "carry-on"
class Hazmat(Item):
    rule = "hazmat"
    tag = "red"
class PowerBank(CarryOn, Hazmat):
    pass

print([c.__name__ for c in PowerBank.__mro__])
# ['PowerBank', 'CarryOn', 'Hazmat', 'Item', 'object']
print(PowerBank.rule, PowerBank.tag)
# carry-on red
  • rule は CarryOn で先に見つかりました。かっこの中の左の親が先です。
  • tag は CarryOn にないので、次の枠の Hazmat で見つかりました。Item の "plain" までは下りていきません。
  • Item は一度だけ、しかも二つの子がどちらも終わったあとで出てきます。左の道を最後まで掘り進む深さ優先ではありません。

下の探索機で親の順番を入れ替えてみてください。最後の規格のように、順番を作れない場合も押してみましょう。

MRO探索機

探す属性

探す順番(__mro__)

どこにもない

C3の計算手順
(CarryOn, Hazmat) では rule を CarryOn で、(Hazmat, CarryOn) では Hazmat で見つけます。(Item, Hazmat) はMROを作れないので、クラスがそもそも作られません。

9-3. super() は親ではなく次の枠 (10分)

いよいよ届出人の実際のコードです。規定ごとに検査項目を返し、上の規定の項目をつなげていきます。

class Item:
    def check(self):
        return ["item"]
class CarryOn(Item):
    def check(self):
        return ["size"] + Item.check(self)
class Hazmat(Item):
    def check(self):
        return ["fire"] + super().check()
class PowerBank(CarryOn, Hazmat):
    pass

print(PowerBank().check())
# ['size', 'item']

CarryOn を書いた人は「自分の親は Item」と考えて、Item.check(self) を直接呼びました。単独で使うなら正しいのです。しかし PowerBank のMROでは、CarryOn の次の枠は Item ではなく Hazmat です。直接呼んだせいで、Hazmat を飛ばしてしまいました。

super() は「自分の親」ではありません。type(self) のMROにおける、いまのクラスの次の枠です。同じ CarryOn.check でも、誰が呼ぶかによって次の枠が変わります。

class CarryOn(Item):
    def check(self):
        return ["size"] + super().check()
class PowerBank(CarryOn, Hazmat):
    pass

print(PowerBank().check())
# ['size', 'fire', 'item']
print(CarryOn().check())
# ['size', 'item']
print(super(CarryOn, PowerBank()).check())
# ['fire', 'item']
  • PowerBank() では、CarryOn → Hazmat → Item の順に鎖がつながります。
  • CarryOn() 単独のときはMROが CarryOn, Item, object なので、super() はそのまま Item です。
  • super(CarryOn, obj) は、「obj のMROで CarryOn の次から探せ」と明示的に書いたものです。かっこなしの super() は、この二つの値を自動で埋めてくれます。

下のタグボードで二つのやり方を比べてみてください。呼び出し枠が積み重なる様子と、実行中の行番号を見ればわかります。

タグボード

ステップ 1 / 0

次に実行する行: 0

実習台を準備中…

いま変わったもの

封印されたかばん(イミュータブル)

super() では行の表示が 6 → 9 → 3 と下りていき、check の枠が三つ積み重なります。三つの枠の self はどれも同じ PowerBank のかばんです。Item.check(self) では 6 → 3 へ直行し、枠は二つだけです。super() を使った関数に付いている __class__ セルが、「いまどのクラスの中にいるか」を教えてくれます。

鎖は、全員が super() を呼んではじめてつながります。一人でも抜けると、それより後ろの枠は実行されません。__init__ も同じです。規格書ごとに受け取る引数が違うなら、自分の分だけを取り出し、残りは **kw で次の枠へ渡します。

class Item:
    pass
class CarryOn(Item):
    def __init__(self, cm=55, **kw):
        self.cm = cm
        super().__init__(**kw)
class Hazmat(Item):
    def __init__(self, wh=100, **kw):
        self.wh = wh
        super().__init__(**kw)
class PowerBank(CarryOn, Hazmat):
    pass

print(vars(PowerBank(cm=20, wh=99)))
# {'cm': 20, 'wh': 99}

CarryOn.__init__ は cm だけを取り出して、wh=99 を渡します。次の枠の Hazmat がそれを受け取り、最後の object.__init__() は空の引数で終わります。

ピンの案内

→ メソッドの中で親クラスの名前を直接書いて呼ぶと(Item.check(self))、多重継承で間に挟まった規格書を飛ばしてしまいます。引き継いで呼ぶときは、いつも super() を使いましょう。

9-4. C3線形化:順番を決める規則 (6分)

PythonはMROをC3線形化という方法で作ります。守る約束は三つです。

  1. 子は必ず親より前に来る。
  2. かっこの中に書いた親の順番を守る。
  3. 親それぞれのMROの順番も守る。

PowerBank(CarryOn, Hazmat) の計算はこうなります。親たちのMROと親のリストを並べて置きます。

残っているリスト選んだもの理由
[CarryOn, Item, object] [Hazmat, Item, object] [CarryOn, Hazmat]CarryOn先頭にあり、ほかのリストの後ろの部分にない
[Item, object] [Hazmat, Item, object] [Hazmat]HazmatItem は2番目のリストの後ろの部分にあるので後回し
[Item, object] [Item, object]Itemもう後ろの部分にない
[object] [object]object

結果は、PowerBank のあとに CarryOn, Hazmat, Item, object です。探索機の「C3の計算手順」を開くと、同じ過程が4行で見えます。各行の先頭がそのとき選ばれたクラスで、その後ろがそのとき残っていたリストです。

約束をすべて守れないときは、クラスが作られません。

class Item:
    pass
class Hazmat(Item):
    pass
class Odd(Item, Hazmat):
    pass
# TypeError: Cannot create a consistent method resolution order (MRO) for bases Item, Hazmat

かっこの順番からすると Item は Hazmat より前でなければならず(約束2)、子が先という規則からすると Hazmat は Item より前でなければなりません(約束1)。両方を満たす並びはありません。直し方は簡単で、より具体的な親を左に置きます。

9-5. ミックスイン、isinstance、そしてコンポジション (6分)

ミックスインは、単独では使わず、ほかのクラスに機能を一つ混ぜ込む小さなクラスです。名前の最後に Mixin を付け、親のリストの左側に置きます。そうすればMROで先に見つかります。

class ReprMixin:
    def __repr__(self):
        fields = ", ".join(f"{k}={v!r}" for k, v in vars(self).items())
        return f"{type(self).__name__}({fields})"

class Tag(ReprMixin):
    def __init__(self, code, kg):
        self.code, self.kg = code, kg

print([Tag("ICN", 12)])
# [Tag(code='ICN', kg=12)]

MRO探索機

探す属性

探す順番(__mro__)

どこにもない

C3の計算手順
LabelMixin を左に置くと describe はミックスインで見つかり、右に置くと Item のものが先に見つかって、ミックスインの describe は隠れます。重ならない prefix は、どちらでも見つかります。

isinstance(obj, C) は、type(obj) のMROに C があるかどうかを見ます。issubclass はクラス同士で同じことを尋ねます。だから type(x) is C より範囲が広いのです。

class Item:
    pass
class Hazmat(Item):
    pass
class PowerBank(Hazmat):
    pass

pb = PowerBank()
print(isinstance(pb, Item), issubclass(PowerBank, Hazmat), isinstance(pb, (int, Hazmat)))
# True True True
print(type(pb) is Hazmat)
# False

継承は「AはBの一種だ」というときに使います。「AはBを持っている」なら、コンポジション(ほかのオブジェクトを属性として持つこと)のほうが向いています。届出人の規定も、継承の代わりに規定オブジェクトのリストとして持てば、順番が目に見え、MROを気にする必要もなくなります。

class Rule:
    def __init__(self, name, test):
        self.name, self.test = name, test

class Inspector:
    def __init__(self, *rules):
        self.rules = list(rules)
    def failed(self, item):
        return [r.name for r in self.rules if not r.test(item)]

size = Rule("size", lambda it: it["cm"] <= 55)
fire = Rule("fire", lambda it: it["wh"] <= 100)
print(Inspector(size, fire).failed({"cm": 20, "wh": 160}))
# ['fire']

9-6. 抽象規格書と、子の規格の登録 (3分)

abc.ABC を継承して @abstractmethod を付けると、「子が必ず埋めなければならない枠」になります。埋めていない子は、インスタンスを作れません。__init_subclass__ は、子クラスが作られるたびに親で呼ばれる取っ手なので、規格の登録簿を作るときに使います。

from abc import ABC, abstractmethod

class Rule(ABC):
    registry = []
    def __init_subclass__(cls, **kw):
        super().__init_subclass__(**kw)
        Rule.registry.append(cls.__name__)
    @abstractmethod
    def check(self, item): ...

class SizeRule(Rule):
    def check(self, item):
        return item["cm"] <= 55
class FireRule(Rule):
    pass

print(Rule.registry, SizeRule().check({"cm": 40}))
# ['SizeRule', 'FireRule'] True
FireRule()
# TypeError: Can't instantiate abstract class FireRule without an implementation for abstract method 'check'

事件解決 (10分)

CarryOn の Item.check(self) を super().check() に変え、MROと一緒に検査表をもう一度出します。

class Item:
    def check(self):
        return ["item"]
class CarryOn(Item):
    def check(self):
        return ["size"] + super().check()
class Hazmat(Item):
    def check(self):
        return ["fire"] + super().check()
class PowerBank(CarryOn, Hazmat):
    pass

print([c.__name__ for c in PowerBank.__mro__])
# ['PowerBank', 'CarryOn', 'Hazmat', 'Item', 'object']
print(PowerBank().check())
# ['size', 'fire', 'item']

検査表に fire が戻ってきました。ハン課長がうなずきます。「親を呼ぶんじゃなくて、次の順番を呼ぶべきだったんですね」班長が二枚の規格書を綴じ直してくれます。「順番はPythonが決める。私たちは鎖を切らなければそれでいい」

解決報告 — LOST-009。CarryOn が親の名前を直接呼んだため、MROの次の枠 Hazmat を飛ばしていた。すべての規格書が super() を呼ぶように直して解決。

練習問題

  1. class A: pass、class B(A): pass、class C(A): pass、class D(C, B): pass のMROを、まずC3の表で計算してから D.__mro__ で確かめてください。
  2. 事件解決のコードで Hazmat.check だけを return ["fire"] に変えたら(super() を呼ばなければ)、検査表がどうなるか予想してみてください。
  3. 9-5の Inspector に重さの規定を一つ足し、規定の順番を入れ替えると、結果のリストの順番がどう変わるか確かめてください。

班長の業務手帳

  • 属性は Cls.__mro__ の順に探す。オーバーライドは、先に見つかったものが勝つというだけ。

  • 多重継承では、共通の親は一度だけ、子たちがすべて終わったあとに来る(C3線形化)。

  • super() は親ではなく、type(self) のMROにおける次の枠。全員が呼ばないと鎖はつながらない。

  • __init__ の鎖は、自分の引数だけを取り出し、残りは **kw で渡す。

  • MROを作れなければ、クラスはそもそも作られない。具体的な親を左に。

  • ミックスインは左に。isinstance はMROを見る。「持っている」なら継承よりコンポジション。

← 前へ

8限目 · 空港の標準手順:ダンダーメソッド

次へ →

10限目 · 検査場:属性アクセスの秘密

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

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

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

質問と回答

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

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

R

ReflexKR

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

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

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

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

Built with Reflex