파이썬의 비밀스러운 삶: super()와 메서드 해석 순서

발행: (2025년 12월 3일 오후 02:49 GMT+9)
6 min read
원문: Dev.to

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()

결국 DB를, BA를 상속한다. 상속은 이렇게 동작하는 것이 맞지 않을까?

하지만 코드를 실행했을 때 그는 다음과 같은 결과를 얻었다.

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__)

출력

(, , ,
 , )

이 튜플은 파이썬이 메서드와 속성을 검색하는 순서이다.

  1. D
  2. B
  3. C
  4. A
  5. object

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()

실행 흐름

  1. D.process() → “D.process()” 출력, super()B.process()
  2. B.process() → “B.process()” 출력, super()C.process()
  3. C.process() → “C.process()” 출력, super()A.process()
  4. 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를 결정한다. 이 알고리즘은 다음 세 가지 규칙을 따른다.

  1. 자식이 부모보다 먼저 (local precedence order)
  2. 좌‑우 순서 (상속 리스트에서 왼쪽부터 오른쪽)
  3. 단조성 (부모 MRO에서의 순서를 유지)

단조성은 어떤 부모의 MRO에서 클래스 A가 클래스 B보다 먼저 나타난다면, 자식의 MRO에서도 AB보다 먼저 나타나야 함을 보장한다(명시적으로 재정의하지 않는 한).

예시: D(B, C)의 MRO 구축

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

C3 알고리즘은 다음과 같이 동작한다.

  1. 클래스 자체부터 시작
    L(D) = D + merge(L(B), L(C), [B, C])

  2. 부모 선형화 확장

    L(B) = B, A, object
    L(C) = C, A, object
    L(D) = D + merge([B, A, object], [C, A, object], [B, C])
  3. 병합 (다른 어떤 꼬리에도 나타나지 않는 첫 번째 머리를 선택)

    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를 호출하는 이유를 설명한다.

Back to Blog

관련 글

더 보기 »

Java OOPS 개념

Forem 로고https://media2.dev.to/dynamic/image/width=65,height=,fit=scale-down,gravity=auto,format=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%...

문자열 비교는 사전식 민감도

Day 78 – 2025년 12월 2일 나는 아직 Day 3 & 4 목표인 “Day 3‑4: Control structures if‑else, loops”와 Day 5 목표를 뒤처지고 있기 때문에 집중해서 해야겠다.

모뎀의 복수

첫 번째 연결 1994년 겨울, 홍콩의 작은 아파트에서, 14세 소년이 US Robotics Sportster 14,400 Fax Modem을 연결했다.