파이썬 코딩의 기술 - 40

JinWooHyun·2021년 7월 28일
0

파이썬 코딩의 기술

목록 보기
10/14

super로 부모 클래스를 초기화하라

자식 클래스에서 부모 클래스를 초기화하는 방법은 자식 인스턴스에서 부모 클래스의 __init__ 메서드를 직접 호출하는 것이다.

class myBaseClass:
    def __init__(self, value):
        self.value = value

class MyChildClass(MyBaseClass):
    def __init__(self):
        MyBaseClass.__init__(self, 5)

이 방법은 기본적인 클래스 게층의 경우에는 잘 작동하지만, 어떤 클래스가 다중 상속에 의해 영향을 받은 경우 상위 클래스의 __init__ 메서드를 직접 호출하면 프로그램이 예측할 수 없는 방식으로 작동할 수 있다.

다중 상속을 사용하는 경우 모든 하위 클래스에서 __init__ 호출의 순서가 정해져 있지 않다는 것이다.

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)

이 클래스의 인스턴스를 만들면 부모 클래스의 순서에 따라 초기화가 실행된다.

foo = OneWay(5)

>>>
(5 * 2) + 5 = 15

다음 코드는 같은 부모 클래스를 사용하지만 부모 클래스를 나열한 순서가 다른 경우이다.

class AnotherWay(MyBaseClass, PlusFive, timesTwo):
    def __init__(self, value):
        MyBaseClass.__init__(self, value)
        TimesTwo.__init__(self)
        PlusFive.__init__(self)

하지만 부모 클래스의 생성자를 호출하는 순서는 그대로 뒀기 때문에 인스턴스를 만들면 OneWay의 인스턴스와 똑같은 결과가 나온다.

bar = AnotherWay(5)

>>>
(5 * 2) + 5 = 15

또한 다이아몬드 상속이 이루어지면 공통 조상 클래스의 __init__ 메서드가 여러 번 호출될 수 있기 때문에 코드가 예기치 않은 방식으로 작동할 수 있다.

다이아몬드 상속
어떤 클래스가 두 가지 서로 다른 클래스를 상속하는데, 두 상위 클래스의 상속 계층에 같은 조상 클래스가 존재하는 경우

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)

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

두 번째 부모 클래스의 생성자 PlusNine.__init__를 호출하면 MyBaseClass.__init__이 다시 호출되면서 self.value가 다시 5로 돌아간다.

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

class TimesSevenCorrect(MyBaseClass):
    def __init__(self, value):
        super().__init__(self, value)
        self.value *= 7

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

이제는 다이아몬드 정점에 있는 MyBaseClass.__init__이 단 한번만 실행된다. 다른 부모 클래스의 생성자는 class 문에 지정된 순서대로 호출된다.

class GoodWay(TimesSevenCorrect, PlusNineCorrect):
    def __init__(self, value):
        super().__init__(value)

foo = GoodWay(5)

>>>
7 * (5 + 9) = 98

호출 순서는 이 클래스에 대한 MRO 정의를 따른다. MRO 순서는 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'>

호출은 순서대로 진행되고 상속 다이아몬드의 정점에 도달하면 각 초기화 메서드는 각 클래스의 __init__이 호출된 순서의 역순으로 작업을 수행하게 된다.

super().__init__ 호출은 다중 상속을 튼튼하게 해주며, 하위 클래스에서 MyBaseClass.__init__을 직접 호출하는 것보다 유지 보수를 더 편하게 해준다. 나중에 최상위 클래스의 이름을 바꾼다거나 상속받는 상위 클래스를 변경하더라도 각각의 __init__ 메서드 정의를 바꿀 필요가 없다.

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

class ExplicitTrisect(MyBaseClass):
    def __init__(self, value):
        super(ExplicitTrisect, self).__init__(value)
        self.value /= 3

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

class AutomaticTrisect(MyBaseClass):
    def __init__(self, value):
        super(__class__, self).__init__(value)
        self.value /= 3

class ImplicitTrisect(MyBaseClass):
    def __init__(self, value):
        super().__init__(value)
        self.value /= 3

따라서 위의 세 가지 사용법은 모두 동일하다. super에 파라미터를 제공해야 하는 유일한 경우는 자식 클래스의 특정 기능에 접근해야 하는 경우뿐이다. (특정 기능을 감싸거나 재사용해야 하는 경우)

기억해야 할 내용

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

0개의 댓글