super, c3 linearization, MRO에 대해서
21 Jan 2022
[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
-
그렇다면 이제 다이아 몬드 상속 문제가 생기는 경우를 살펴보자.
다이아몬드 상속 문제는 공통 조상 클래스의 생성자(
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가지를 만족시키면 된다.
- The parents MRO’s remain consistent
- The local MRO’s remain consistent
- No cyclicality
다음 예시를 생각해보면 쉽게 이해될꺼 같다.
이때 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에 파라미터 제공해야 하는 유일한 경우는 자식 클래스에서 상위클래스의 특정 기능에 접근해야 하는 경우 뿐이다.