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

전민수·2023년 7월 3일
0

EffectivePython

목록 보기
8/10

자식 클래스에서 부모 클래스를 초기화하는 전통적인 방법

  • 자식 인스턴스에서 부모 클래스의 __init__ 메서드를 직접 호출하는 방법
class MyBaseClass:
    def __init__(self, value):
        self.value = value

class MyChildClass(MyBaseClass):
    def __init__(self):
        MyBaseClass.__init__(self, 5) # << 부모 클래스의 __init__ 메서드 호출

이 방법의 문제점

1. 다중 상속 시

다중 상속 : 두 개 이상의 부모 클래스를 상속하는 경우
모든 하위 클래스에서 __init__호출의 순서가 정해져 있지 않음

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 OneWay(MyBaseClass, TimesTwo, PlusFive):
    def __init__(self, value):
        MyBaseClass.__init__(self, value)
        TimesTwo.__init__(self)
        PlusFive.__init__(self)

class AnotherWay(MyBaseClass, PlusFive, TimesTwo):
    def __init__(self, value):
        MyBaseClass.__init__(self, value)
        TimesTwo.__init__(self)
        PlusFive.__init__(self)
foo = OneWay(5)
print('첫 번째 부모 클래스 순서에 따른 값은 (5 * 2) + 5 =', foo.value)

# 첫 번째 부모 클래스 순서에 따른 값은 (5 * 2) + 5 = 15

bar = AnotherWay(5)
print('두 번째 부모 클래스 순서에 따른 값은', foo.value)

# 두 번째 부모 클래스 순서에 따른 값은 (5 * 2) + 5 = 15

클래스 정의에서 부모 클래스를 나열한 순서와 부모 클래스의 생성자를 호출하는 순서가 일치하지 않음.

2. 다이아몬드 상속 시

다이아몬드 상속 : 같은 조상을 가지는 두 클래스를 부모 클래스로 상속하는 경우

class MyBaseClass:
    def __init__(self, value):
        self.value = value
        
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)

foo = ThisWay(5)
print('(5 * 7) + 9 = 44가 나와야 하지만 실제로는', foo.value)

# (5 * 7) + 9 = 44가 나와야 하지만 실제로는 14

TimesSeven.__init__ 생성자에서 수행한 계산은 무시됨.

해결책 : super 내장 함수와 MRO

super : 다이아몬드 계층의 공통 상위 클래스를 단 한 번만 호출하도록 보장

MRO : 상위 클래스를 초기화하는 순서를 정의

참고 > C3 선형화 알고리즘 사용 (추후 공부)

  1. super
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)

foo = GoodWay(5)
print('7 * (5 + 9) = 98이 나와야 하고 실제로도', foo.value)

# 7 * (5 + 9) = 98이 나와야 하고 실제로도 98

조상 클래스, BaseClass.__init__이 단 한 번만 호출되어 의도한대로 코드가 동작

하지만,

class GoodWay(TimesSevenCorrect, PlusNineCorrect)

라는 부분을 보면 정의부에 TimesSevenCorrect가 먼저 나왔고 PlusNineCorrect가 나중에 나왔으니 (5*7)+9 가 되어야 하는 거 아닌가? 라는 의문이 들 수 있다.

이에 대한 대답은 다음과 같다.

호출 순서는 이 클래스에 대한 MRO 정의를 따른다.

mro_str = '\n'.join(repr(cls) for cls in GoodWay.mro())

>>>
<class '__main__.GoodWay'> # 4
<class '__main__.TimesSevenCorrect'> # 3
<class '__main__.PlusNineCorrect'> # 2
<class '__main__.MyBaseClass'> # 1
<class 'object'>

super().__init__ 호출은 다중 상속을 튼튼하게 해주며, 하위 클래스에서 MyBaseClass.__init__을 직접 호출하는 것보다 유지 보수를 더 편하게 함

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

하지만 object 인스턴스를 초기화할 때는 두 파라미터를 지정할 필요가 없다. 아무 인자도 지정하지 않고 super를 호출하면, 파이썬 컴파일러가 자동으로 올바른 파라미터 (__class__와 self)를 넣어준다.

profile
Learning Mate

0개의 댓글