ChangHyeon Nam's Blog notes and thoughts

super, c3 linearization, MRO에 대해서

Comments

[Effective Python] Better way 40: super로 부모 클래스를 초기화 하라

  • 파이썬은 표준 메서드 결정 순서(MRO)를 활용해 상위 클래스 초기화 순서와 다이아몬드 상속 문제를 해결한다.
  • 부모 클래스를 초기화할 때는 super 내장 함수를 아무 인자 없이 호출하라. super를 아무 인자 없이 호출하면 파이썬 컴파일러가 자동으로 옵바른 파라미터를 넣어준다.

Better way 40에서는 다중 상속을 받을 경우 생길 수 있는 문제인 다이아몬드 상속 문제를 super() 를 이용하여 해결하는 것을 설명한다. 먼저 def __init__ 만을 이용해 다중 상속을 하여 다이아몬드 상속에서의 문제를 보이고, 그다음 MRO와 super를 통해 이를 해결하는 과정의 내용이다.

  • def __init__ 만을 이용해서 다중 상속하는 코드는 다음과 같다.

      class Parent:
          def __init__(self,value):
              self.value=value
    
      class Child_times_two:
          def __init__(self):
              self.value*=2
    
      class Child_plus_Five:
          def __init__(self):
              self.value+=5
    
      class Grandchild(Parent,Child_times_two,Child_plus_Five):
          def __init__(self,value):
              Parent.__init__(self,value)
              Child_times_two.__init__(self)
              Child_plus_two.__init__(self)
    
      foo = Grandchild(5)
      print(f'클래스 생성자에 따른 순서의 결과 :{foo.value}')
      # 클래스 생성자에 따른 순서의 결과 :15
    
  • 그렇다면 이제 다이아 몬드 상속 문제가 생기는 경우를 살펴보자.

    Untitled

    다이아몬드 상속 문제는 공통 조상 클래스의 생성자(def __init__)이 여러번 호출되어 생기는 문제이다.

      class Parent:
          def __init__(self,value):
              self.value=value
    
      class Child_times_two(Parent):
          def __init__(self,value):
              Parent.__init__(self,value)
              self.value*=2
    
      class Child_plus_Five(Parent):
          def __init__(self,value):
              Parent.__init__(self,value)
              self.value+=5
    
      class Grandchild(Child_times_two,Child_plus_Five):
          def __init__(self,value):
              Child_times_two.__init__(self,value)
              Child_plus_Five.__init__(self,value)
    
      foo = Grandchild(3)
      print(f'클래스 생성자에 따른 순서의 결과 :{foo.value}')
      # 클래스 생성자에 따른 순서의 결과 :8
    

    Child_plus_Fived 클래스에 대한 생성자만 호출되고, 앞서 호출된 Child_times_two 클래스의 생성자는 무시되는 것을 확인할 수 있다. 공통 조상인 Parent 클래스의 생성자를 두번 호출하여, value attribute를 초기화 해줘서 생긴 문제점이다.

  • 이러한 문제를 해결하기 위해 파이썬에서는 super 라는 내장함수와 Method Resolution Order (MRO)가 있다. super 를 사용하며 다이아몬드 계층의 공통 상위 클래스를 단 한번만 호출하도록 보장한다. MRO는 상위 클래스를 초기화 하는 순서를 정의한다. 이때 C3 linearization 이라는 알고리즘을 이용하여 순서를 결정한다.

    C3 linearization은 다음 기준 3가지를 만족시키면 된다.

    1. The parents MRO’s remain consistent
    2. The local MRO’s remain consistent
    3. No cyclicality

    다음 예시를 생각해보면 쉽게 이해될꺼 같다.

    Untitled

    이때 MRO : A→B→E→C→D→F→G이다.

    super를 이용했을때 코드는 다음과 같다.

      class Parent:
          def __init__(self,value):
              self.value=value
    
      class Child_times_two(Parent):
          def __init__(self,value):
              super().__init__(value)
              self.value*=2
    
      class Child_plus_Five(Parent):
          def __init__(self,value):
              super().__init__(value)
              self.value+=5
    
      class Grandchild(Child_times_two,Child_plus_Five):
          def __init__(self,value):
              super().__init__(value)
    
      foo = Grandchild(3)
      print(f'클래스 생성자에 따른 순서의 결과 :{foo.value}')
      # 클래스 생성자에 따른 순서의 결과 :16
    

    이때, Child_times_two의 생성자가 Child_plus_Five 의 생성자보다 먼저 불리는게 맞지 않나 생각이 들었다. 위의 코드에 대한 mro를 출력해보면 다음과 같다.

      mro_str = '\n'.join(repr(cls)for cls in Grandchild.mro())
      print(mro_str)
      # <class '__main__.Grandchild'>
      # <class '__main__.Child_times_two'>
      # <class '__main__.Child_plus_Five'>
      # <class '__main__.Parent'>
      # <class 'object'>
    

    즉, 다이아몬드의 정점에 도달하면 각 초기화 메서드는 각 클래스의 __init__ 이 호출된 순서의 역순으로 작업을 수행한다. super().__init__ 호출은 다중 상속을 튼튼하게 해주며, 하위클래스를 직접 호출하는 것보다 유지 보수를 편리하게 해준다.

  • 또한 super 함수에 두가지 parameter를 전달할 수 있다. 첫번째 파라미터는 접근하고 싶은 MRO 뷰를 제공할 부모 타입이고, 두번째 파라미터는 첫번째 파라미터로 지정한 타입의 MRO 뷰에 접근할 때, 사용할 인스턴스이다.

      class Grandchild(Child_times_two,Child_plus_Five):
          def __init__(self,value):
              super(Grandchild,self).__init__(value)
    
  • 하지만 object 인스턴스를 초기화할 때는 두 파라미터를 지정할 필요가 없다. 클래스 정의 안에서 아무 인자도 지정하지 않고 super를 호출하면, 파이썬 컴파일러가 자동으로 올바른 파라미터(__class__와 self)를 넣어준다.

      class Grandchild(Child_times_two,Child_plus_Five):
          def __init__(self,value):
              super(__class__,self).__init__(value)
    

    바로 위의 코드와 동일한 동작을 한다.

    super에 파라미터 제공해야 하는 유일한 경우는 자식 클래스에서 상위클래스의 특정 기능에 접근해야 하는 경우 뿐이다.


Reference

  1. Does Python’s MRO, C3 linearization work depth-first? Empirically it does not