[Python] Skill of coding - super로 부모 클래스를 초기화 하자

Hyeseong·2020년 12월 10일
1

python skill of coding

목록 보기
18/18

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

기존에는 자식 클래스에서 부모 클래스의 init 메서드를 직접 호출하는 방법으로 부모 클래스를 초기화했어요.

class MyBaseClass(object):
    def __init__(self, value):
        self.value = value

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

    def times_two(self):
        return self.value * 2
10

이 방법은 간단한 계층 구조에는 잘 동작하지만 많은 경우 제대로 동작하지 못한다.

클래스가 다중 상속(보통은 피해야할 방법)의 영향을 받는다면 슈퍼클래스의 __init__ 메서드를 직접 호출하는 행위는 예기치 못한 동작을 일으킬 수 있다.

한 가지 문제는 __init__의 호출 순서가 모든 서브클래스에 걸쳐 명시되어 있지 않다는 점이다. 예를 들어 value 필드로 연산을 수행하는 부모 클래스 두 개를 정의해보자.


class TimesTwo(object):
    def __init__(self):
        self.value *= 2

class PlusFive(object):
    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)
print('First ordering is (5 * 2) + 5 =', foo.value)
First ordering is (5 * 2) + 5 = 15

다음은 같은 부모 클래스들을 다른 순서로 정의한 클래스다.



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

다음은 같은 부모 클래스 생성자 Plus.File.__init__, TimesTwo.__init__를 이전과 같은 순서로 호출한다. 이 클래스의 동작은 부모 클래스를 정의한 순서와 일치하지 않는다.


bar = AnotherWay(5)
print('Second ordering still is', bar.value)
Second ordering still is 15

다른 문제는 다이아몬드 상속이다. 다이아 몬드 상속은 서브 클래스가 계층 구조에서 같은 슈퍼 클래스를 둔 서로 다른 두 클래스에서 상속 받을 때 발생한다. 다이아몬드 상속은 공통 슈퍼 클래스의 init 메서드를 여러 번 실행하게 해서 예상치 못한 동작을 일으킨다. 예를 들어 MyBaseClass에서 상속받는 자식 클래스 두개를 정의해보자

class TimesFive(MyBaseClass):
    def __init__(self, value):
        MyBaseClass.__init__(self, value)
        self.value *= 5

class PlusTwo(MyBaseClass):
    def __init__(self, value):
        MyBaseClass.__init__(self, value)
        self.value += 2

다음으로 이 두 캘르스 모두에서 상속받는 자식 클래스를 정의하여 MyBaseClass를 다이아몬드의 꼭대기로 만든다.

class ThisWay(TimesFive, PlusTwo):
    def __init__(self, value):
        TimesFive.__init__(self, value)
        PlusTwo.__init__(self, value)

foo = ThisWay(5)
print('Should be (5 * 5) + 2 = 27 but is', foo.value)
Should be (5 * 5) + 2 = 27 but is 7

(5 * 5) + 2 = 27이므로 결과는 27이어야한다. 하지만 두 번째 부모 클래스의 생성자 PlusTwo.__init__를 호출하는 코드가 있어서 MyBaseClass.__init__(self, value)가 두 번째 호출될 때 self.value를 다시 5로 리셋한다.

파이썬에서는 이 문제를 해결하려고 super라는 내장 함수를 추가하고 메서드 해석 순서(MRO, Method Resolution Order)를 정의했는데요. MRO는 어떤 슈퍼클래스부터 초기화하는지를 정해요.(예를 들면 깊이 우선, 왼쪽에서 오른쪽으로). 또한 다이아몬드 계층 구조에 있는 공통 슈퍼 클래스를 단 한번만 실행하게 해요.

다음 코드는 다이아몬드 클래스 구조지만 super로 부모클래스를 초기화해요.


class MyBaseClass(object):
    def __init__(self, value):
        self.value = value

class TimesFiveCorrect(MyBaseClass):
    def __init__(self, value):
        super(TimesFiveCorrect, self).__init__(value)
        self.value *= 5

class PlusTwoCorrect(MyBaseClass):
    def __init__(self, value):
        super(PlusTwoCorrect, self).__init__(value)
        self.value += 2

이제 다이아몬드의 꼭대기인 MyBaseClassinit가 한번만 실행되요. 다른 부모 클래스는 class 문으로 지정한 순서대로 실행되요.


class GoodWay(TimesFiveCorrect, PlusTwoCorrect):
    def __init__(self, value):
        super(GoodWay, self).__init__(value)

foo = GoodWay(5)
print('should be 5*(5+2)=35 and is', foo.value)
should be 5*(5+2)=35 and is 35

이 순서는 뒤에서부터 시작하는 것 같아요. TimesFiveCorrect.__init__를 먼저 실행할 수는 없을까요? 그래서 결과가 (5*5)+2 = 27이 되게 할 수는 없을까? 정답은 그럴수 없다. 이 순서는 이 클래스에 대해 MRO가 정의하는 순서와 일치한다. MRO순서는 mro라는 클래스 메서드로 알 수 있다.



from pprint import pprint

pprint(GoodWay.mro())
[<class '__main__.GoodWay'>,
 <class '__main__.TimesFiveCorrect'>,
 <class '__main__.PlusTwoCorrect'>,
 <class '__main__.MyBaseClass'>,
 <class 'object'>]

파이썬 3부터는 super를 사용해야 한다. super는 명확하고 간결하며 항상 제대로 동작한다.


class Explicit(MyBaseClass):
    def __init__(self, value):
        super(__class__, self).__init__(value * 2)

class Implicit(MyBaseClass):
    def __init__(self, value):
        super().__init__(value * 2)

assert Explicit(10).value == Implicit(10).value

__class__ 변수를 사용한 메서드에서 현재 클래스를 올바르게 참조하도록 해주므로 위의 코드가 잘 동작한다.

핵심정리

  • 파이썬 표준 메서드 해석 순서는 슈퍼 클래스의 초기화 순서와 다이아몬드 상속 문제를 해결한다.
  • 항상 내장함수 super로 부모 클래스를 초기화 하자
profile
어제보다 오늘 그리고 오늘 보다 내일...

0개의 댓글