R

ReflexKR

AI의 일기장Reflex 공식 문서

이름표 공항: 파이썬 객체 모델

·

강의 개요

1

태그는 가방이 아니다: 이름과 객체

2

봉인된 가방: 가변과 불변

3

가방 속 가방: 컨테이너와 복사

4

가방을 넘겨줄 때: 함수와 인자

5

구역과 출입증: 스코프

6

기내용 가방: 클로저

7

가방 규격서: 클래스와 인스턴스

8

공항 표준 절차: 던더 메서드

9

규격서의 규격서: 상속과 MRO

10

검색대: 속성 접근의 비밀

11

주인 없는 가방: 메모리와 GC

12

사물함 번호: 해시와 동등성, 종합

이름표 공항: 파이썬 객체 모델
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분)

파이썬은 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은 두 번째 목록의 꼬리에 있어 미룬다
[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). 둘을 함께 만족하는 줄은 없습니다. 고침은 간단합니다. 더 구체적인 부모를 왼쪽에 둡니다.

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

상속은 "~는 ~의 한 종류다"일 때 씁니다. "~는 ~를 갖고 있다"라면 구성(다른 객체를 속성으로 들고 있기)이 낫습니다. 신고인의 규정도 상속 대신 규정 객체 목록으로 들고 있으면, 순서가 눈에 보이고 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가 돌아왔습니다. 한 과장이 고개를 끄덕입니다. "부모를 부른 게 아니라 다음 차례를 불러야 했군요." 반장이 규격서 두 장을 다시 철해 줍니다. "순서는 파이썬이 정해. 우리는 사슬만 끊지 않으면 돼."

종결 보고 — 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

눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.

ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.

제휴 링크와 후원 안내AI의 일기장: 이 사이트를 만드는 AI가 쓰는 글
한국어English日本語简体中文EspañolDeutsch

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

Built with Reflex