brunch

You can make anything
by writing

C.S.Lewis

by 이권수 Jun 23. 2024

Python MRO란?

다중 상속 시, 검색 순서를 결정하는 MRO

이메일로 블로그 포스트 받아보기: https://blog.techchallengearena.com/


Python에서 MRO(Method Resolution Order)는 다중 상속 시 메서드나 속성을 어떤 순서로 검색할지를 결정하는 규칙을 의미한다. MRO는 Python의 super() 함수와 밀접한 관련이 있으며, 이는 다중 상속에서의 메서드 호출 순서를 명확하게 이해하는 데 필수적이다. Python의 MRO는 C3 선형화(C3 Linearization) 알고리즘을 기반으로 결정된다. 

__mro__ 는 MRO 에 대한 순서를 보여주는 변수이다.


C3 Linearization 알고리즘

C3 Linearization은 객체 지향 프로그래밍에서 클래스의 상속 순서를 결정하는 알고리즘이다. 이 알고리즘은 클래스가 다중 상속을 사용할 때, 메서드 해결 순서를 명확하게 정의해준다. Python에서 클래스가 상속을 통해 메서드나 속성을 상속받을 때, 어떤 순서로 이를 처리할지를 결정하는 데 중요한 역할을 한다.


C3 Linearization의 주요 목적은 모호성을 제거하고, 예측 가능한 메서드 해상도 순서를 제공하는 것이다. 이를 통해 다중 상속 시에 발생할 수 있는 잠재적인 충돌을 예방할 수 있다. 예컨대, 두 개 이상의 부모 클래스가 동일한 이름의 메서드를 가지고 있을 때, 어느 부모 클래스의 메서드를 우선시할지를 명확히 규정한다.


C3 Linearization의 동작 원리는 다음과 같다.

모든 부모 클래스의 리스트를 구한다.

각 리스트를 병합하여 전체 상속 순서를 계산한다. 이때, 각 리스트의 첫 번째 요소가 상속 순서의 앞쪽에 위치하도록 한다.

병합 과정에서 모순이 발생하지 않도록 순서를 유지한다.


C3 Linearization 알고리즘을 cpython에서 어떻게 구현해서 사용하고 있는지 확인해보자. cpython의 functools 코드 중 _c3_mro() 함수를 보면, 알고리즘이 구현되어 있다.

먼저, 현재 클래스(cls) 의 __bases__에 들어있는 클래스에 대해서 _c3_mro() 함수를 수행한다. __bases__는 현재 클래스가 상속하고 있는 부모 클래스를 담고 있다. 즉, 부모 클래스의 c3_mro 결과를 먼저 구하는 과정을 거친다.


그리고 나서, _c3_merge() 과정을 거친다. _c3_merge() 코드를 보면 다음과 같다.

sequences에는 합치고자하는 모든 MRO 값들이 들어있다. 예컨대, A(B, C) 라고 한다면, [class A, class B, class C, object] 와 같은 순서로 되어 있으며, 이러한 리스트가 여러개 들어있는게 바로 sequences이다. 


알고리즘을 보면 다음과 같다.

1.  sequences 를 순회하면서 각 리스트에서 가장 첫번째 원소를 꺼낸다.

2. 만약 해당 원소가 다른 모든 sequences의  두번째 원소 이후에 존재하지 않는다면 해당 값을 먼저 result에 넣는다. 다시 말하면, 해당 원소는 무조건 sequences의 첫번째 원소에만 존재해야 한다. 첫번째 원소에만 있다는 이야기는 뒤에 나오는 부모 클래스에서 해당 원소를 상속하고 있지 않다는 의미이기 때문이다.

3. 만약 원소를 찾으면 전체 sequences에서 해당 원소를 제거한다. 

4. sequences가 모두 빈 리스트가 될때까지 해당 과정을 반복한다.


이해를 돕기 위해 아래와 같이 코드를 작성한 경우, 어떻게 MRO가 결정되는지 알아보도록 하자. 

위 예시에서 D의 MRO를 계산하는 과정은 다음과 같다.


위 과정을 자세히 보면, __bases__ 에 어떤 클래스가 먼저 들어가있느냐에 따라서 결과 값이 달라지는 것을 알 수 있다. 즉, 상속할때, 부모 클래스를 상속하는 순서에 따라서 Python이 MRO를 구성한다는 의미이다. 그래서 상속할때는 순서를 정확하게 구성하는 것이 중요하다. 그렇지 않으면 같은 이름으로 되는 함수를 만들었을때 원치않는 결과가 발생할 수도 있다.


다음의 예시를 보면 상속 순서에 따라서 어떤 함수가 호출되는지가 결정되는지 명확하게 알 수 있다.



Super() 함수의 동작 원리

super() 함수는 파이썬에서 다중 상속을 처리하고, 부모 클래스의 메서드를 호출하는 데 사용되는 내장 함수이다. 이 함수는 메서드 해상도 순서(Method Resolution Order, MRO)를 따르며, 다중 상속 구조에서도 일관된 방식으로 메서드를 찾을 수 있도록 도와준다.


super() 함수를 호출하면 어떤 과정을 거치는지 cpython 코드에 상세하게 설명되어 있다.

super_getattro() 함수를 보면, MRO를 순회하면서, 호출한 함수가 dict에 있는지를 확인한다. 가장 먼저 매칭되는 함수가 구현되어 있는 Class의 함수를 반환한다. 만약 super(Class, obj) 형태로 호출한다고 하면, obj의 MRO를 순회하면서, Class 다음에 오는 부모 객체를 반환한다.


요컨대, 파이썬은 내부적으로 상속에 대한 일관성을 유지하고, 중복되는 함수에 대한 모호성을 제거하기 위해서 MRO라는 규칙을 사용한다. 클래스에 매핑되는 매서드는 상속 구조에 따라 달라지는데, 이를 확인하기 위해서는 __mro__를 확인해보면 된다. 따라서, 상속을 할때는 반드시 순서에 유의해야 한다.


작가의 이전글 사설 대역 사용 시, docker 주의사항
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari