분실 신고서
LOST-009
신고인
항공 화물 규정 담당 개발자 한 과장
잃어버린 것
보조배터리 규격 PowerBank의 위험물 검사. '기내 반입' 규정과 '위험물' 규정을 모두 물려받게 했는데, 검사 결과에 위험물 항목이 없다.
해결 조건
PowerBank가 규정을 읽는 순서(MRO)를 밝히고, 두 규정이 모두 실행되게 고친다.
하늘섬 국제공항 수하물 센터
한 과장은 보조배터리 규격을 새로 만들었습니다. 기내 반입 규격서와 위험물 규격서를 둘 다 물려받게 했으니 검사도 둘 다 될 줄 알았습니다. 그런데 검사표에는 크기 검사만 있고 위험물 검사가 없습니다.
도미란 반장이 규격서 두 장을 겹쳐 봅니다. "상위 규격서가 둘이면, 어느 쪽을 먼저 읽는지가 정해져 있어. 그 순서부터 보자고."
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 Trueb.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교시).
신고인의 규격은 두 규격서를 물려받습니다. 둘 다 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 redrule은 CarryOn에서 먼저 찾았습니다. 괄호 안 왼쪽 부모가 먼저입니다.tag는 CarryOn에 없으니 다음 칸인 Hazmat에서 찾았습니다. Item의 "plain"까지 내려가지 않습니다.Item은 한 번만, 그리고 두 자식이 모두 끝난 뒤에 나옵니다. 왼쪽 길을 끝까지 파고드는 깊이 우선이 아닙니다.아래 탐색기에서 부모 순서를 바꿔 보고, 마지막 규격처럼 순서를 만들 수 없는 경우도 눌러 보세요.
MRO 탐색기
찾을 속성
찾는 순서 (__mro__)
어디에도 없음
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()를 불러야 이어집니다. 한 명이라도 빠지면 그 뒤 칸은 실행되지 않습니다. __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()를 쓰세요.
파이썬은 MRO를 C3 선형화라는 방법으로 만듭니다. 지키는 약속은 셋입니다.
PowerBank(CarryOn, Hazmat)의 계산은 이렇습니다. 부모들의 MRO와 부모 목록을 나란히 놓습니다.
| 남은 목록 | 고른 것 | 이유 |
|---|---|---|
[CarryOn, Item, object] [Hazmat, Item, object] [CarryOn, Hazmat] | CarryOn | 첫 머리. 다른 목록의 꼬리에 없다 |
[Item, object] [Hazmat, Item, object] [Hazmat] | Hazmat | Item은 두 번째 목록의 꼬리에 있어 미룬다 |
[Item, object] [Item, object] | Item | 이제 꼬리에 없다 |
[object] [object] | object |
결과는 PowerBank 뒤에 CarryOn, Hazmat, Item, object입니다. 탐색기의 "C3 계산 단계"를 펼치면 같은 과정이 네 줄로 보입니다. 줄마다 맨 앞이 이번에 뽑힌 클래스이고, 뒤는 그때 남아 있던 목록들입니다.
약속을 모두 지킬 수 없으면 클래스가 만들어지지 않습니다.
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). 둘을 함께 만족하는 줄은 없습니다. 고침은 간단합니다. 더 구체적인 부모를 왼쪽에 둡니다.
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__)
어디에도 없음
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상속은 "~는 ~의 한 종류다"일 때 씁니다. "~는 ~를 갖고 있다"라면 구성(다른 객체를 속성으로 들고 있기)이 낫습니다. 신고인의 규정도 상속 대신 규정 객체 목록으로 들고 있으면, 순서가 눈에 보이고 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']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'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가 돌아왔습니다. 한 과장이 고개를 끄덕입니다. "부모를 부른 게 아니라 다음 차례를 불러야 했군요." 반장이 규격서 두 장을 다시 철해 줍니다. "순서는 파이썬이 정해. 우리는 사슬만 끊지 않으면 돼."
종결 보고 — LOST-009. CarryOn이 부모 이름을 직접 불러 MRO의 다음 칸 Hazmat을 건너뛰었음. 모든 규격서가 super()를 부르도록 고쳐 해결.
class A: pass, class B(A): pass, class C(A): pass, class D(C, B): pass의 MRO를 C3 표로 먼저 계산한 뒤 D.__mro__로 확인하세요.Hazmat.check만 return ["fire"]로 바꾸면(super()를 부르지 않으면) 검사표가 어떻게 될지 예측해 보세요.Inspector에 무게 규정 하나를 더하고, 규정 순서를 바꿔 결과 목록의 순서가 어떻게 달라지는지 확인하세요.반장의 업무 수첩
속성은 Cls.__mro__ 순서로 찾는다. 오버라이드는 먼저 찾은 것이 이기는 것뿐.
다중 상속에서 공통 부모는 한 번만, 자식들이 다 끝난 뒤에 온다(C3 선형화).
super() 는 부모가 아니라 type(self) 의 MRO에서 다음 칸. 모두가 불러야 사슬이 이어진다.
__init__ 사슬은 자기 인자만 꺼내고 나머지는 **kw 로 넘긴다.
MRO를 만들 수 없으면 클래스가 아예 만들어지지 않는다. 구체적인 부모를 왼쪽에.
믹스인은 왼쪽에. isinstance 는 MRO를 본다. '갖고 있다'면 상속보다 구성.
새 교시·새 강의 소식 받기
이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.
R
ReflexKR
눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.
ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.
© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
질문과 답변
이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.