Effective Python Item 40. super로 부모 클래스를 초기화 하라.

    들어가기 전

     


    요약

    • 파이썬에서는 부모 클래스가 자동으로 초기화 되지 않는다.
    • 다중 상속보다는 믹스인 클래스로 합성해라. 
    • 다중 상속을 사용할 경우, 메서드의 실행 순서는 mro를 따른다. (Class.mro() 호출한 순서대로 메서드가 호출됨). 
    • [부모 클래스].__init__()를 이용해 직접 부모 클래스를 초기화 할 수 있다.
      • mro 순서와는 다르게 부모 클래스가 초기화 됨. (개발자에게 혼란을 가져옴)
      • 이렇게 할 경우, 다이아몬드 상속에서 부모 클래스가 여러 번 초기화 된다. (개발자에게 혼란을 가져옴)
      • 부모 클래스의 이름이 바뀌면, 초기화 하는 코드도 같이 바뀌어야 한다. (확장에 닫혀있음) 
    • super().__init__()로 부모 클래스를 초기화하면, 다이아몬드 상속에서도 부모 클래스가 1번만 초기화 된다. 

     


    Item 40. super()로 부모 클래스를 초기화 하라.

    파이썬에서는 부모 클래스가 자동으로 초기화 되지 않는다. 부모 클래스를 초기화 하려면 명시적으로 [부모_클래스_이름].__init__() 메서드를 호출해야한다. 

    class Parent:
      def __init__(self):
          pass
    
    class ChildA(Parent):
      def __init__(self):
          Parent.__init__(self)

    이렇게 작성된 코드는 부모 클래스를 정상적으로 초기화 해준다. 그러나 위 코드는 한 가지 문제를 가지고 있는데, 자식 클래스에서 부모 클래스를 초기화 하는 부분이 하드코딩 되어 있다는 점이다. 이렇게 작성된 코드에서 부모 클래스의 이름이 바뀐다면, 부모 클래스를 초기화 하는 부분도 같이 변경되어야 한다는 것을 의미한다. 즉, 확장하기 어려운 코드가 된다. 

    [부모_클래스_이름].__init__() 메서드로 부모 클래스를 초기화 하는 것의 단점은 이 뿐만이 아니다. 다중 상속 시에도 몇 가지 문제점을 가진다.

    class MyBaseClass:
        def __init__(self, value):
            print(f'MyBaseClass __init__() : {value}')
            self.value = value
    
    class TimeTwo(MyBaseClass):
        def __init__(self, value):
            MyBaseClass.__init__(self, value)
            self.value *= 2
    
    class PlusFive(MyBaseClass):
        def __init__(self, value):
            MyBaseClass.__init__(self, value)
            self.value += 5
    
    class ThisWay(PlusFive, TimeTwo):
        def __init__(self, value):
            TimeTwo.__init__(self, value)
            PlusFive.__init__(self, value)

    클래스 Thisway는 다중상속하는 코드다. 여기서 [부모_클래스_이름].__init__()로 부모 클래스를 초기화 하는 것의 문제점을 살펴볼 수 있다. 

    1. 상속받은 클래스 순으로 부모 클래스가 초기화 되어야한다. 그러나 TimeTwo.__init__()를 호출했기 때문에 TimeTwo 클래스가 먼저 초기화 된다. 
    2. TimeTwo, PlusFive는 다이아몬드 상속 클래스다. 이 때, 공통 조상인 MyBaseClass가 두 번 초기화 된다. 

    [부모_클래스_이름].__init__()는 다중상속에서 위 두 가지 문제를 야기시킬 수 있다. 두 가지 모두 개발자가 의도하지 않는 결과를 만들어 낼 수도 있기 때문에 큰 문제가 된다. 위 코드에서는 조상 클래스인 MyBaseClass가 두번 초기화 되기 때문에 +5가 된 값에 *2가 된 값이 된다. (아래 코드에서 자세히 살펴보자)

    따라서 [부모_클래스_이름].__init__()를 이용해 직접 부모 클래스를 초기화 하는 것은 지양해야한다. 

    class MyBaseClass:
        def __init__(self, value):
            print(f'MyBaseClass __init__() : {value}')
            self.value = value
    
    
    class TimeTwo(MyBaseClass):
        def __init__(self, value):
            super().__init__(value)
            self.value *= 2
    
    class PlusFive(MyBaseClass):
        def __init__(self, value):
            super().__init__(value)
            self.value += 5
    
    class ThisWay(TimeTwo, PlusFive):
        def __init__(self, value):
            super().__init__(value)

    그 대신에 super().__init__()로 부모 클래스를 초기화하면 모든 단점이 해결된다.

    1. 하드코딩된 부모 클래스 초기화 
    2. 다중상속에서 상속받은 클래스 순서와 실제 부모 클래스 초기화 순서가 다름
    3. 다이아몬드 상속에서 공통 조상 클래스가 여러 번 초기화 됨. 

    결론적으로 super().__init__()로 부모 클래스를 초기화하라는 의미다. 

     


    코드1 : 다중 상속에서 super().__init__()로 초기화 하지 않았을 때 문제점

    # 그러나 이 방식은 다중 상속의 경우, 부모 클래스가 여러 번 호출된다는 문제가 있음. (특히 다이아몬드 상속에서 문제)
    # 이런 형태로 의도치 않게 여러 번 호출되는 경우, 개발자가 알기 어려워 짐.
    
    class MyBaseClass:
        def __init__(self, value):
            print(f'MyBaseClass __init__() : {value}')
            self.value = value
    
    class TimeTwo(MyBaseClass):
        def __init__(self, value):
            MyBaseClass.__init__(self, value)
            self.value *= 2
    
    class PlusFive(MyBaseClass):
        def __init__(self, value):
            MyBaseClass.__init__(self, value)
            self.value += 5
    
    class ThisWay(TimeTwo, PlusFive):
        def __init__(self, value):
            TimeTwo.__init__(self, value)
            PlusFive.__init__(self, value)
    
    foo = ThisWay(5)
    for klass in ThisWay.mro():
        print(klass)
    # print(ThisWay.mro())
    print(f'(5 + 5) * 2 = 20이 나와야 하지만 실제로는 {foo.value}')

    위 코드는 다이아몬드 상속 조건에서 [부모클래스_이름].__init__()를 호출해 부모 클래스를 초기화 했을 때의 문제점을 나타낸다.

    1. 상속 순서에 따라서 (5+5) * 2가 실행될 것이 기대된다.
    2. 그러나 코드 상에서는 (5*2)가 저장된 다음에 다시 한번 (5+5)의 값이 저장된다.
      • 그렇게 동작하는 이유는 인스턴스에 저장한 value 값을 재사용하는 것이 아니라, __init__()에서 받은 value라는 값을 사용하기 때문이다.
    (5 + 5) * 2 = 20이 나와야 하지만 실제로는 10

    코드를 실행해보면, 20이 나와야 할 값이 10이 나오는 것을 확인할 수 있다. 

    위 코드의 실행은 꽤나 추론하기 어렵다. 

    1. mro 순서대로 실행되지도 않는다.
    2. 인스턴스 value를 사용하는 것이 아니라 파라메터로 주어진 value를 그대로 사용하는 상태이기 때문에 원하는대로 동작하지도 않는다. 

    이 문제들은 [부모클래스_이름].__init__()로 직접 부모 클래스를 초기화 했기 때문에 발생하는 문제다. 아래에서 super().__init__()를 이용해 이 문제를 해결하는 것을 살펴보자.

     


    코드2 : 다중 상속에서 super().__init__()로 초기화 했을 때 결과

    # 다이아몬드 상속에서 부모클래스 초기화가 여러번 되는 것을 막기 위해 다음을 사용할 수 있음.
    # 1. super().__init__()로 초기화한다.
    # 2. super().__init__()는 각 클래스가 가지고 있는 mro(표준 메서드 결정 순서)에 따라서 순차적으로 부모 클래스를 초기화함.
    # 3. 따라서 공통 부모 클래스가 단 한번만 초기화 된다.
    # 4. 실행되는 순서는 mro()인데, 코드 블락은 반대로 처리된다.
    #    Thisway init() -> TimeTwo init() -> PlusFive init() -> MyBassClass init() 및 init() 블록 처리 -> PlusFive init() 블록 처리 -> TimeTwo init() 블록 처리 -> Thisway init() 블록 처리
    
    class MyBaseClass:
        def __init__(self, value):
            print(f'MyBaseClass __init__() : {value}')
            self.value = value
    
    class TimeTwo(MyBaseClass):
        def __init__(self, value):
            super().__init__(value)
            print(1)
            self.value *= 2
    
    class PlusFive(MyBaseClass):
        def __init__(self, value):
            super().__init__(value)
            print(2)
            self.value += 5
    
    class ThisWay(TimeTwo, PlusFive):
        def __init__(self, value):
            super().__init__(value)
    
    foo = ThisWay(5)
    for m in ThisWay.mro():
        print(m)
    print(f'(5 + 5) * 2 = 20이 나와야 하지만 실제로는 {foo.value}')
    

     

    위 코드는 super()를 이용해 부모 클래스를 초기화한다. 과연 어떻게 동작할까?

    1. super()는 mro 순서대로 부모 클래스의 __init__() 함수를 호출한다. 
    2. 이 때, 이미 호출된 부모 클래스의 __init__() 함수라면 중복해서 호출하지 않는다. 
    3. 각 클래스의 __init__() 함수 블록이 처리되는 순서는 __init__()가 호출된 반대 순서대로 처리된다.
      1. __init__() 함수 호출 순서 : Thisway → TimeTwo → PlusFive → MyBaseClass
      2. __init__() 함수 블록 실행 순서 : MyBaseClass → PlusFive → TimeTwo → ThisWay

     

     

     

    (5 + 5) * 2 = 20이 나와야 하지만 실제로는 20

    코드 실행 결과도 기대한 것처럼 동작한다. 정리하면 다음과 같다.

    • mro 순서대로 처리되는 것을 확인할 수 있음. 
    • 부모 클래스 초기화가 중복해서 처리되지 않음. 

    super().__init__()로 부모 클래스를 초기화 하는 것의 장점은 바로 '개발자가 기대하던대로 동작한다'라는 점이다. 다중상속은 그렇지 않아도 복잡한 형태의 구조라 오히려 믹스인 클래스들을 이용해 믹싱하는 것을 추천하는데, 생각한 것처럼 동작하지 않으면 정말 사용하기 어려운 코드가 된다. super().__init__()를 이용한다면, 그나마 다중상속을 덜 어렵게 사용할 수 있게 만들어준다. 

    따라서, 다중상속이 필요한 경우라면 반드시 super().__init()를 이용해 부모 클래스를 초기화 하는 것을 추천한다. 

    댓글

    Designed by JB FACTORY