Home [Better Way #40] super로 부모 클래스를 초기화하라 (+ MRO)
Post
Cancel

[Better Way #40] super로 부모 클래스를 초기화하라 (+ MRO)

본문은 “파이썬 코딩의 기술 (Effective Python, 2판)”“Chapter 05. Classes and Interfaces”을 읽고 정리한 내용입니다.


자식 클래스에서 부모 클래스를 초기화하는 올바른 방법에 대해 알아보자.


부모 클래스의 __init__ 메서드를 직접 호출할 때의 문제점

부모 클래스를 초기화할 때, 부모 클래스의 __init__ 메서드를 직접 호출하는 것은 권장하지 않는 방법이다. 그 이유는 무엇일까?

  • 다중 상속을 사용하는 경우, 상위 클래스의 __init__ 메서드를 직접 호출하면 모든 하위 클래스에서 __init__ 호출의 순서가 정해져 있지 않아 프로그램이 예측할 수 없는 방식으로 작동할 수 있다.

    다음과 같이 클래스 정의에서 부모 클래스를 나열한 순서부모 클래스의 생성자를 호출하는 순서가 일치하지 않는다면 잘못된 결과를 얻을 수 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    class MyBaseClass:
        def __init__(self, value):
            self.value = value
              
    class TimesTwo:
        def __init__(self):
            self.value *= 2
              
    class PlusFive:
        def __init__(self):
            self.value += 5
      
    class AnotherWay(MyBaseClass, PlusFive, TimesTwo):
        def __init__(self, value):
            # 부모 클래스의 순서와 부모 클래스의 생성자를 호출하는 순서가 일치하지 X
            MyBaseClass.__init__(self, value)
            TimesTwo.__init__(self)
            PlusFive.__init__(self)
    
    1
    2
    3
    
    foo = AnotherWay(5)
    print(f"(5 + 5) * 2 = 20이 나와야 하지만, 실행 결과는 {foo.value}")
    # (5 + 5) * 2 = 20이 나와야 하지만, 실행 결과는 15
    
  • 다이아몬드 상속을 사용하는 경우, 공통 조상 클래스의 __init__ 메서드가 여러 번 호출될 수 있으므로 코드가 예기치 못한 방식으로 작동할 수 있다.

    다이아몬드 상속

    어떤 클래스가 두 가지 서로 다른 클래스를 상속하는데, 두 상위 클래스의 상속 계층을 거슬러 올라가면 같은 조상 클래스가 존재하는 경우이다.

    다음과 같은 경우, 상속 다이아몬드의 정점에 있는 MyBaseClass.__init__이 두 번 실행되어, 중간에 값이 다시 초기화되므로 예상한 결과가 출력되지 않는다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    
    class TimesSeven(MyBaseClass):
        def __init__(self, value):
            MyBaseClass.__init__(self, value)
            self.value *= 7
              
    class PlusNine(MyBaseClass):
        def __init__(self, value):
            MyBaseClass.__init__(self, value)
            self.value += 9
      
    class ThisWay(TimesSeven, PlusNine):
        def __init__(self, value):
            TimesSeven.__init__(self, value)
            PlusNine.__init__(self, value)      # -- 호출 시, MyBaseClass.__init__이 다시 호출되면서 self.value가 다시 5로 돌아간다!
    
    1
    2
    3
    
    foo = ThisWay(5)
    print(f"(5 * 7) + 9 = 44가 나와야 하지만, 실행 결과는 {foo.value}")
    # (5 * 7) + 9 = 44가 나와야 하지만, 실행 결과는 14
    


부모 클래스를 초기화 할 때는 super().__init__을 호출하자

1
2
3
4
5
6
7
8
9
10
11
12
13
class TimesSevenCorrect(MyBaseClass):
    def __init__(self, value):
        super().__init__(value)
        self.value *= 7

class PlusNineCorrect(MyBaseClass):
    def __init__(self, value):
        super().__init__(value)
        self.value += 9

class GoodWay(TimesSevenCorrect, PlusNineCorrect):
    def __init__(self, value):
        super().__init__(value)
1
2
3
foo = GoodWay(5)
print(f"7 * (5 + 9) = 98이 나와야 하며, 실제 실행 결과도 {foo.value}")
# 7 * (5 + 9) = 98이 나와야 하며, 실제 실행 결과도 98
  • super는 상속 다이아몬드 계층의 공통 상위 클래스를 단 한 번만 호출하도록 보장한다.
  • super를 사용하면 추후 공통 상위 클래스의 이름을 변경하거나, 하위 클래스가 상속 받는 상위 클래스를 변경하더라도 각각의 __init__ 메서드 정의를 바꿀 필요가 없다.
  • 두 가지 파라미터를 넘길 수 있는데, object 인스턴스를 초기화할 때는 두 파라미터를 지정할 필요가 없으므로 부모 클래스를 초기화할 때는 아무 인자 없이 호출하자!

    파이썬 컴파일러가 자동으로 올바른 파라미터(__class__, self)를 넣어주기 때문이다.

    1
    2
    3
    
    # 다음은 동일하다.
    super(__class__, self).__init__(value)
    super().__init__(value)
    
    • 두 가지 파라미터의 종류

      첫 번째 파라미터접근하고 싶은 MRO 뷰를 제공할 부모 타입
      두 번째 파라미터지정한 타입의 MRO 뷰에 접근할 때 사용할 인스턴스
    • super에 두 파라미터를 제공해야 하는 경우는 자식 클래스에서 상위 클래스의 특정 기능에 접근해야 하는 경우 뿐이다.


그런데 GoodWay<TimesSevenCorrect, PlusNineCorrect> 순서로 상속받도록 하였는데, 실제 실행 결과를 살펴보면 PlusNineCorrect의 동작이 먼저 일어나 + 9가 우선적으로 실행되게 된다. 그 이유는 MRO 때문이다!


MRO (Method Resolution Order)

  • 상위 클래스를 초기화하는 순서를 정의하며, C3 linearization이라는 알고리즘을 사용한다.
  • 파이썬은 MRO를 통해 상위 클래스 초기화 순서다이아몬드 상속 문제를 해결한다.
  • 각 클래스의 __init__이 호출된 순서의 역순으로 실제 초기화 작업이 수행된다!

    1
    2
    
    for cls in GoodWay.mro():   # 클래스 메서드 mro()를 통해 MRO 순서를 살펴본다
        print(repr(cls))
    

  • 참고로, GoodWay상속 순서를 반대로 하면 다음과 같은 결과가 나오게 된다.

    1
    2
    3
    
    class GoodWay(PlusNineCorrect, TimesSevenCorrect):  # -- 상속 순서 반대
        def __init__(self, value):
            super().__init__(value)
    
    1
    2
    3
    
    foo = GoodWay(5)
    print(f"순서가 바뀌어 (7 * 5) + 9 = 44이 나와야 하며, 실제 실행 결과도 {foo.value}")
    # 순서가 바뀌어 (7 * 5) + 9 = 44이 나와야 하며, 실제 실행 결과도 44
    
    1
    2
    
    for cls in GoodWay.mro():   # 클래스 메서드 mro()를 통해 MRO 순서를 살펴본다
        print(repr(cls))
    

This post is licensed under CC BY 4.0 by the author.

[Better Way #39] 객체를 제너릭하게 구성하려면 @classmethod를 통한 다형성을 활용하라

[Better Way #41] 기능을 합성할 때는 믹스인 클래스를 사용하라