ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python]: 다중상속과 메서드 결정 순서 MRO(Method Resolution Order)
    Python/Python 2024. 6. 27. 14:30
    반응형

    다중상속이 무엇인가

    Python에서 다중 상속은 하나의 클래스가 둘 이상의 부모 클래스로부터 속성과 메서드를 상속받는 기능을 의미합니다. 다중 상속은 매우 유용하지만, 복잡성을 증가시키기 때문에 신중하게 사용해야 합니다. Python의 다중 상속에서 주의해야 할 주요 특징과 개념은 다음과 같습니다.

     

    MRO( Method Resolution Order )는 무엇인가?

    MRO는 다중 상속 시 메서드나 속성을 검색하는 순서를 결정하는 규칙입니다.


    파이썬은 MRO는 C3 슈퍼클래스 선형화(C3 linearization)에 기반한다.

     

    C3 선형화 메서드로 변경하기 이전에는 하나의 클래스가 두 개의 조상을 가졋을 경우, 다중 상속을 계단식으로 사용하지 않는 단순한 경우만 고려했기에 계산과 추적이 매우 쉬웠따~.

     

    MRO가 생기기전에는 간단히 깊이 우선 depth-first으로 검색했다.

    class CommonBase:
        pass
    
    
    class Base1(CommonBase):
        pass
    
    
    class Base2(CommonBase):
        def method(self):
            print("Base2.method() called()")
    
    
    class MyClass(Base1, Base2):
        pass

    왼쪽에서 오른쪽으로 탐색하는 깊이 우선left-to-right depth-first 규칙에 따라 동작한다. 단순한 알고리즘 결정 순서에서는 Base클래스를 따라 최상위까지 올라온 뒤, 다시 Base2를 탐색한다. [아래 그림 참조]

    다이아몬드 클래스 계층( diamond class hierachy )

     

    하지만.

    표준라이브러리는 이런 구조의 상속 계층을 갖지 않으며, 이를 나쁜 프랙티스라고 생각한다고 한다.

    파이썬에서는 이런상속이 가능하므로 보다 명확하게 잘 정의해야 한다.
    Object 클래스 타입의 최상위 계층에 위치한다. 모든 클래스는 필연적으로 큰 다이아몬드 클래스 상속 계층의 일부가 된다. 이는 C언어 측면에서 해결되어야 하는것이다.

     

    이런 이유에서 파이썬은 MRO 알고리즘으로 C3선형화를 사용한다고 한다.

    파이썬 MRO 참조문서에서는 선형화에 관해서 기술한다.

    C의 선형화는 C와 그 부모(언어)의 선형화 병합, 부모(언어)의 리스트를 합친 것이다.

     

    앞의 상속 예시에 심벌릭 표기법은

    L[MyClass(Base1, Base2)] = [MyClass] + merge(L[Base1], L[Base2], [Base1, Base2])

     

    이런식으로 표기 된다.

    - L은 클래스에 대한 MRO 리스트를 저장하는 사전(dictionary)입니다.
    - L[MyClass(Base1, Base2)]: MyClass가 Base1과 Base2를 상속할 때의 MRO 리스트를 의미합니다.
    - = [MyClass]: 새로운 클래스 MyClass를 포함한 리스트로 초기화합니다.
    - + merge(L[Base1], L[Base2], [Base1, Base2]): Base1과 Base2의 MRO 리스트를 병합하여 새로운 MRO 리스트를 생성합니다.

     

    여기에서 L[MyClass]는 MyClass의 선형화이며, merge는 여러 선형화 결과를 병합하는 특정한 알고리즘이다.

    merge 알고리즘은 중복을 줄이고 올바른 순서를 보존한다. 이 알고리즘은 리스트 헤드([0])와 테일([-1]) 개념을 이용한다. 만약 이헤드가 어떤 리스트에 테일에 포함되지 않으면 헤드를 MyClass의 선형화에 추가한다. 그리고 헤드를 Merge의 리스트에서 제거한다. 그렇지 않으면 다음 리스트의 헤드를 찾고 좋은 헤드라면 그것을 선택한다.

    결론적으로

    1. 깊이 우선 룩업을 모든 부모에 대해서 실행해서 시퀀스를 얻는다.
    2. 왼쪽에서 오른쪽으로 검색하는 규칙을 적용해 모든 리스트를 병합함으로 써 한 클래스가 여러 리스트에포함되어 있을 때 계층을 명확하게 한다.

    심벌릭 단계를 통해 MyClass MRO를 계산해야 한다면 모든 L[Class]를 선형화를 풀어낸다.

    MRO 계산방법을 작성 할 것인데 글로 설명을 다 표현하기 어려우므로 직접 해보시길. (머리가 나빠서 작성자는 이해하는데 좀 오래걸림;;;) 

    선형화 과정

    L[MyClass] = [MyClass] + merge(L[Base1], L[Base2], [Base1, Base2])
        = [MyClass] + merge(
            [Base1 + merge(L[CommonBase], [CommonBase])],
            [Base2 + merge(L[CommonBase], [CommonBase])],
            [Base1, Base2]
            )
        = [MyClass] + merge(
            [Base1] + merge(L[CommonBase], [CommonBase]),
            [Base2] + merge(L[CommonBase], [CommonBase]),
            [Base1, Base2]
            )
        = [MyClass] + merge(
            [Base1] + merge([CommonBase] + merge(L[Object]), [CommonBase]),
            [Base2] + merge([CommonBase] + merge(L[Object]), [CommonBase]),
            [Base1, Base2]
        )

    요소가 하나인 리스트 [Object]이다. 이는 merge([object])를 [object]로 계속 풀어낼 수 있음을 의미한다.

    = [MyClass] + merge(
        [Base1] + merge([CommonBase] + merge[object]), [CommonBase]),
        [Base2] + merge([CommonBase] + merge[object]), [CommonBase]),
        [Base1, Base2]
    )

    merge[object]는 요소가 하나인 리스트이므로 [object]로 풀어낼 수 있다.

    = [MyClass] + merge(
        [Base1] + merge([CommonBase, object], [CommonBase]),
        [Base2] + merge([CommonBase, object], [CommonBase]),
        [Base1, Base2]
    )

    이제 merge([CommonBase, object], [CommonBase])를 풀어낸다. 첫 번째 리스트의 헤드는 CommonBase이며 다른 리스트의 테일에 속하지 않는다. merge에서 CommonBase를 제거한 뒤 바깥선형 결과에 추가한다.

    = [MyClass] + merge(
        [Base1, CommonBase] + merge([object]),
        [Base2, CommonBase] + merge([object]),
        [Base1, Base2]
    )

    merge([object])가 계속 남아 있으므로 계속 풀어낸다.

    = [MyClass] + merge(
        [Base1, CommonBase, object],
        [Base2, CommonBase, object],
        [Base1, Base2]
    )

    마지막 merge가 남아 있으면 이는 간단하지 않다. non-trival. 첫 번째 헤드는 Bsae1이다. Base1은 다른 리스트 테일에 속하지 않는다. merge에서 제거한 뒤 바깥 선형화 결과에서 추가한다.

    = [Myclass] + merge(
        [CommonBase, object],
        [Base2, CommonBase, object],
        [Base2]
    )

    이제 첫 번째 헤드는 CommonBase이다. 이는 두 번째 list [Base2, CommonBase, object]의 테일에 속한다. 이는 현 시점에서 더는 진행할 수 없으므로 다음 헤드인 Base2로 이동해야 함을 의미한다. Base2는 다른 리스트의 테일에 속하지 않는다. merge에서 Base2를 제거한 뒤 바깥 선형화 결과에 추가한다.

    = [MyClass, Base1, Base2] + merge(
        [CommonBase, object],
        [CommonBase, object],
        []
    )

     

    CommonBase가 다시 첫 번째 헤드가 되었으며, 이제 다른 리스트 테일에 속하지 않는다. merge에서 Base1을 제거한 뒤 바깥 선형화 결과에 추가한다.

    = [MyClass, Base1, Base2, CommonBase] + merge([object], [object], [])

    마지막 merge 결과를 거치면 최종결과는

    [Myclass, Base1, Base2, CommonBase, object]

    다. 

    실제로 MyClass.__mro__ 로 출력해보면

    (<class '__main__.MyClass'>, <class '__main__.Base1'>, <class '__main__.Base2'>, <class '__main__.CommonBase'>, <class 'object'>)

     

    같은 결과를 가질 수 있따. 따라서 우리는 다중 상속시 위와같은 MRO 방식을 인지하고 사용해야 한다. 

    다중 상속 사용 시 주의점

    • 복잡성: 다중 상속은 클래스 관계를 복잡하게 만들 수 있습니다. 이를 관리하기 위해서는 설계를 신중히 해야 합니다.
    • 명확성: 클래스 간의 관계가 명확하지 않을 경우 유지보수가 어려울 수 있습니다.
    • 충돌: 동일한 이름의 메서드나 속성이 여러 부모 클래스에 존재할 경우 충돌이 발생할 수 있습니다.

     

    결론

    MRO를 사용하여 순서를 확인하고 super()를 활용하여 적절히 값에 접근하는것으로 효과적으로 관리할 수 있따.

    반응형

    댓글

Designed by Tistory.