파이썬의 비밀스러운 삶: super()와 메서드 해석 순서
Source: Dev.to
super()가 “부모 클래스”를 의미하지 않는 이유 (그리고 실제로 하는 일)
Timothy는 화면을 바라보며 완전히 당황했다. 그는 겉보기엔 간단해 보이는 상속 코드를 작성했지만, 출력 결과는 전혀 의미가 없었다.
class A:
def process(self):
print("A.process()")
class B(A):
def process(self):
print("B.process()")
super().process()
class C(A):
def process(self):
print("C.process()")
super().process()
class D(B, C):
def process(self):
print("D.process()")
super().process()
d = D()
d.process()
그는 다음과 같은 출력이 나올 것이라고 기대했다.
D.process()
B.process()
A.process()
결국 D는 B를, B는 A를 상속한다. 상속은 이렇게 동작하는 것이 맞지 않을까?
하지만 코드를 실행했을 때 그는 다음과 같은 결과를 얻었다.
D.process()
B.process()
C.process()
A.process()
메서드 해석 순서 (Method Resolution Order, MRO)
“
super()는 당신의 부모 클래스를 호출하는 것이 아니라, 메서드 해석 순서(MRO)에서 다음 클래스를 호출하는 거야,” 라고 Margaret가 설명했다.
파이썬의 모든 클래스는 __mro__ 라는 속성을 가지고 있다—메서드 해석 순서이다. 이는 파이썬이 속성을 찾을 때 따라가는 정확한 순서를 정의하는 튜플이다.
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B, C):
pass
print(D.__mro__)
출력
(, , ,
, )
이 튜플은 파이썬이 메서드와 속성을 검색하는 순서이다.
DBCAobject
super()는 “MRO에서 다음 클래스”를 의미할 뿐, “내 부모 클래스”를 의미하지 않는다.
MRO는 isinstance()와 issubclass()에도 영향을 미친다.
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
obj = D()
print(isinstance(obj, D)) # True
print(isinstance(obj, B)) # True
print(isinstance(obj, C)) # True
print(isinstance(obj, A)) # True
print(isinstance(obj, object)) # True
print(issubclass(D, B)) # True
print(issubclass(D, C)) # True
print(issubclass(D, A)) # True
print(D.__mro__) # isinstance가 True를 반환하는 모든 클래스
원본 예제 추적
class A:
def process(self):
print("A.process()")
class B(A):
def process(self):
print("B.process()")
super().process() # B 뒤의 MRO에서 다음 클래스
class C(A):
def process(self):
print("C.process()")
super().process() # C 뒤의 MRO에서 다음 클래스
class D(B, C):
def process(self):
print("D.process()")
super().process() # D 뒤의 MRO에서 다음 클래스
print(D.__mro__) # (, , , , )
d = D()
d.process()
실행 흐름
D.process()→ “D.process()” 출력,super()→B.process()B.process()→ “B.process()” 출력,super()→C.process()C.process()→ “C.process()” 출력,super()→A.process()A.process()→ “A.process()” 출력
핵심 통찰은 B 안의 super()가 B의 부모(A)를 보지 않고, 인스턴스(D)의 MRO에서 다음 클래스(C)를 본다는 점이다.
인스턴스마다 다른 경로
# B 인스턴스 직접 사용
b = B()
print("MRO of B:", B.__mro__) # (, , )
b.process()
# 출력:
# B.process()
# A.process()
# D 인스턴스
d = D()
print("\nMRO of D:", D.__mro__) # (, , , , )
d.process()
# 출력:
# D.process()
# B.process()
# C.process()
# A.process()
b.process()를 호출하면 MRO가 B → A → object이므로 B 안의 super()는 A를 호출한다. d.process()를 호출하면 MRO가 D → B → C → A → object이므로 B 안의 super()는 C를 호출한다.
파이썬이 MRO를 만드는 방법: C3 선형화
파이썬은 C3 선형화(C3 superclass linearization)라는 알고리즘을 사용해 MRO를 결정한다. 이 알고리즘은 다음 세 가지 규칙을 따른다.
- 자식이 부모보다 먼저 (local precedence order)
- 좌‑우 순서 (상속 리스트에서 왼쪽부터 오른쪽)
- 단조성 (부모 MRO에서의 순서를 유지)
단조성은 어떤 부모의 MRO에서 클래스 A가 클래스 B보다 먼저 나타난다면, 자식의 MRO에서도 A가 B보다 먼저 나타나야 함을 보장한다(명시적으로 재정의하지 않는 한).
예시: D(B, C)의 MRO 구축
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
C3 알고리즘은 다음과 같이 동작한다.
-
클래스 자체부터 시작
L(D) = D + merge(L(B), L(C), [B, C]) -
부모 선형화 확장
L(B) = B, A, object L(C) = C, A, object L(D) = D + merge([B, A, object], [C, A, object], [B, C]) -
병합 (다른 어떤 꼬리에도 나타나지 않는 첫 번째 머리를 선택)
merge([B, A, object], [C, A, object], [B, C]): heads → B, C, B B is not in any tail → take B D, B + merge([A, object], [C, A, object], [C]): heads → A, C, C A is in a tail → skip C is not in any tail → take C D, B, C + merge([A, object], [A, object]): heads → A, A A is not in any tail → take A D, B, C, A + merge([object], [object]): heads → object, object object is not in any tail → take object Final MRO: D, B, C, A, object
따라서 파이썬은 MRO (, , , , )를 계산하고, 이는 인스턴스가 D 타입일 때 B 안의 super()가 C를 호출하는 이유를 설명한다.