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

김승환·2021년 7월 11일

코딩의 기술

목록 보기
30/36

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

  • 자식 클래스에서 부모 클래스를 초기화하는 오래된 방법은 바로 자식 인스턴스에서 부모 클래스의 init 메서드를 직접 호출하는 것
  • 아래 접근 방법은 기본적인 클래스 계층의 경우에는 잘 작동하지만 다른경우에는 잘못될 수 있다.
class MyBaseClass:
    def __init__(self, value):
        self.value = value

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

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

  • 다중 상속을 사용하는 경우 생기는 문제 중 하나는 모든 하위 클래스에서 init 호출의 순서가 정해져 있지 않다는 것이다.
  • 예를 들어 다음 코드는 인스턴스의 value 필드에 대해 작용하는 두 상위 클래스를 정의한다.
class TimesTwo:
    def __init__(self):
        self.value *= 2

class PlusFive:
    def __init__(self):
        self.value += 5

다음 클래스의 정의는 부모 클래스를 TimesTwo,PlusFive 순서로 정의한다.

class OneWay(MyBaseClass, TimesTwo, PlusFive):
    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

  • 다음 코드는 같은 부모 클래스를 사용하지만 부모 클래스 나열 순서가 다른 경우다.
  • 앞코드와 반대로 PlusFive 다음에 TimesTwo가 위치
class AnotherWay(MyBaseClass, PlusFive, TimesTwo): #순서가 바뀜
    def __init__(self, value):
        MyBaseClass.__init__(self, value)
        TimesTwo.__init__(self)
        PlusFive.__init__(self)
        
#class 안에서는 정의 순서대로 작동.
bar = AnotherWay(5)
print('두 번째 부모 클래스 순서에 따른 값은', bar.value)

두 번째 부모 클래스 순서에 따른 값은 15

  • 하지만 부모클래스의 생성자를 호출하는 순서는 그대로 뒀기 때문에 인스턴스를 만들면 OneWay의 인스턴스와 똑같은 결과를 볼 수 있다.
  • 클래스 정의에서 부모 클래스를 나열한 순서와 부모 클래스의 생성자를 호출하는 순서가 일치하지 않는다는 뜻
  • 이런식으로 클래스 정의에 나열한 부모 클래스의 순서와 부모 생성자를 호출한 순서가 달라서 생기는 문제는 발견하기가 쉽지않다.
  • 코드를 처음보고 이해하기 어렵다.
  • 다이아몬드 상속으로 인해 다른 문제가 생길 수도 있다.
  • 다이아몬드 상속이란
    - 어떤 클래스가 두가지 서로 다른 클래스를 상속하는데, 두 상위 클래스의 상속 계층을 거슬러 올라가면 같은 조상 클래스가 존재하는 경우를 말한다.
  • 다이아 몬드 상속이 이뤄지면 공통 조상 클래스의 init 메서드가 여러번 호출될 수 있기 때문에 코드가 예기치 않은 방식으로 작동할 수 있다.
  • 예를 들어 다음 코드는 myBaseClass를 상속하는 두가지 자식 클래스를 정의
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

다음으로 이 두 클래스 모두 상속하는 자식 클래스를 정의해 MyBaseClass를 다이아몬드의 맨 위 꼭지점에 위치시킨다.

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

위 문제를 해결하기 위해 super라는 내장 함수와 표준 메서드 결정 순서가 있다.

  • super를 사용하면 다이아몬드 계층의 공통 상위 클래스를 단 한번만 호출하도록 보장한다.
  • MRO는 상위 클래스를 초기화하는 순서를 정의한다.
  • C3 선형화라는 알고리즘을 사용한다.
  • 다음 코드는 다이아몬드 모양의 클래스구조를 다시만들되, 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

  • 위 코딩 순서상 44가 나와야하는 데 98이 나온다.
  • 이는 MRO 정의를 따른다
  • init는 호출된 순서의 역순을 따른다.
mro_str = '\n'.join(repr(cls) for cls in GoodWay.mro())
print(mro_str)

<class 'main.GoodWay'>
<class 'main.TimesSevenCorrect'>
<class 'main.PlusNineCorrect'>
<class 'main.MyBaseClass'>
<class 'object'>

super 함수에 두 가지 파라미터를 넘길 수 있다.

  • 첫 파라미터는 우리가 접근하고 싶은 MRO 뷰를 제공할 부모타입
  • 두번째는 파라미터는 첫 번째 파라미터로 지정한 타입의 MRO 뷰에 접근할 때 사용할 인스턴스다.
  • 이런 선택적인 파라미터를 생성자 안에서 다음과 같이 쓸수 있다.
class ExplicitTrisect(MyBaseClass):
    def __init__(self, value):
        super(ExplicitTrisect, self).__init__(value)
        self.value /= 3
  • 하지만 object 인스턴스를 초기화할 때는 두 파라미터를 지정할 필요가 없다.
  • 클래스 정의 안에서 아무 인자도 지정하지 않고 super를 호출하면, 파이썬 컴파일러가 자동으로 올바른 파라미터를 넣어준다.(class와 self)
  • 따라서 앞의 ExplicitTrisect와 다음 두가지 사용법은 모두 동일
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

assert ExplicitTrisect(9).value == 3
assert AutomaticTrisect(9).value == 3
assert ImplicitTrisect(9).value == 3
profile
인공지능 파이팅!

0개의 댓글