파이문

파이썬 클래스 상속 본문

Python/Python

파이썬 클래스 상속

민Z 2017. 1. 16. 22:04

파이썬 클래스 상속

(Python class inheritance)



클래스란 무엇인가?

리스트나 문자열과 같은 구조는 정말 유용하지만 때때로 구현하고자 하는 것에 제약이 걸릴 때가 있다. 예를 들면 동물에 대한 정보를 담고 있는 구조를 원한다고 하자. 동물의 이름과 동물의 종에 대해 담고 싶지만 리스트나 문자열에는 내가 저장하고자 하는 정보를 모두 표현할 수가 없다. 이 때 클래스를 사용할 수 있다.


단지 Pet이라는 클래스를 만들고 attribute에 name과 species를 지정하면 된다.


인스턴스란 무엇인가?

클래스를 만들기 이전에 앞서, 중요한 차이점 하나를 알고 있어야 한다. 바로 클래스는 구조를 포함하고 있다는 것이다. 예를 들어 Pet 클래스는 name(이름)과 species(종)을 호출할 수 있지만 그 값을 갖고 있지 않다는 것이다. 이 때 바로 인스턴스를 사용한다.


polly라는 Pet의 인스턴스를 만들고 종엔 Parrot을, 이름엔 "Polly"를 지정하면 된다.

class Pet(object):

    def __init__(self, name, species):
        self.name = name
        self.species = species

    def get_name(self):
        return self.name

    def get_species(self):
        return self.species

    def __str__(self):
        return "%s is a %s" % (self.name, self.species)

이것은 Pet의 클래스를 표현한 것이다. 아직까진 어떠한 Pet도 만들어지지 않았다. 

    polly = Pet("polly", "parrot")
    print(polly) # polly is a parrot

이처럼 polly라는 인스턴스를 생성해야, polly가 탄생한 것이다.

(__str__메서드는 특별한 함수로, 파이썬의 모든 클래스에서 정의할 수 있다. 이름에서 느낌이 오듯이 string과 관련되어 있으며 인스턴스 호출 시 출력되어지는 형태를 정의할 수 있다. repr과 비교되고는 하는데 나중에 기회가 되면 정리해보고 싶다.)


그러나 Pet 클래스로는 충분하지 않을 때가 있다. 조금 더 세분화한 동물 클래스에 대해서 이야기 하고자 하는 것이다. 이 때 바로 상속이 등장하게 된다. 개나, 고양이에 관한 클래스를 정의할 때 Pet에서 쓰이는 구조와 동일할 때가 있다. 사실 정확히 얘기하자면 상속은 is-a 관계로 '개는 동물이다', '고양이는 동물이다' 처럼 '~이다' 일 경우엔 상속을 하여 클래스를 정의해야 하는 것이다.


Dog 클래스는 아래처럼 정의하면 된다.

class Dog(Pet):

    def __init__(self, name, chases_cats):
        Pet.__init__(self, name, "Dog")
        self.chases_cats = chases_cats

물론 위 뿐만 아니라 다른 방식으로도 수퍼 클래스에 대한 생성자를 생성할 수도 있다.

class Cat(Pet):
    def __init__(self, name):
        super(Cat, self).__init__(name, "Cat")

위의 두 가지의 차이점은 super()는 base class를 정의해야 하는 것을 피할 수 있다는 것이다. 즉 생성자 생성시 Pet을 언급할 필요가 없어졌다. 만약에 나중에 다른 클래스를 상속받게 된다고 할 때, 전자의 경우 init 메서드를 수정해야 하지만 후자의 경우엔 단지 상속 받는 대상만 바꾸면 된다. (바꿔말하면 유지보수가 편해지니까 후자를 권장하는 것이다.)


파이썬 3.0 이후에서는 굳이 super안에 Cat을 명시하지 않고 super().__init__(name, "Cat") 으로 사용할 수 있으니 이렇게 사용하면 된다.


덧붙이자면 이것은 다중상속이 없을 때의 이야기이고, 파이썬은 다중 상속을 지원하기 때문에 super()가 아닌 부모 클래스의 __init__을 쓰다 보면 원치 않는 버그를 만나게 될 수도 있다. 그것은 아래에서 얘기하도록 하겠다.


파이썬 다중 상속과 MRO

(Python multiple inheritance and mro -Method Resolution Order-)


파이썬은 자바와는 다르게 다중 상속을 지원한다.

다중 상속을 지원하는 언어에서는 별개의 상위 클래스가 동일한 이름으로 메서드를 구현할 때 발생하는 이름 충돌 문제를 해결해야 한다. (이를 다이아몬드 문제 라고 한다.)


아래의 예제를 살펴보자.

class A:
    def ping(self):
        print('A ping:', self)


class B(A):
    def pong(self):
        print('B pong:', self)


class C(A):
    def pong(self):
        print('C PONG:', self)


class D(B, C):
    def ping(self):
        super().ping()
        print('post-ping:', self)

    def pingpong(self):
        self.ping()
        super().ping()
        self.pong()
        super().pong()
        C.pong(self)


if __name__ == '__main__':
    d = D()
    #d.pong()
    d.pingpong()

클래스 A를 클래스 B와 클래스 C가 상속을 하고, 이 B,C를 클래스 D가 상속받는다. 이 때, D의 인스턴스 d가 pingpong 메서드를 호출하면 결과 값은 다음과 같이 나온다.

A ping: <__main__.D object at 0x01F6E670>
post-ping: <__main__.D object at 0x01F6E670>
A ping: <__main__.D object at 0x01F6E670>
B pong: <__main__.D object at 0x01F6E670>
B pong: <__main__.D object at 0x01F6E670>
C PONG: <__main__.D object at 0x01F6E670>

self.ping() 메서드가 super().ping()과 print('post-ping') 을 호출하고, super.ping()이 A.ping()을 호출하여 위의 결과물에서 1,2번째 라인이 출력되는 것이다. 


3번째 라인의 결과는 super().ping()의 결과이며, 4번째 라인의 결과는 self.pong()이 B.pong()을 호출하였기 때문이다. super().pong()도 같은 이유에서이다. (D에는 pong 메서드를 오버라이드 하지 않았으므로)


마지막 C.pong()은 객체를 인수로 전달해서 슈퍼클래스의 메서드를 직접 호출한 것이다.


어째서 super().pong()이 C.pong()이 아닌 B.pong()을 호출하였는가에 관한 문제는 바로 MRO(Method Resolution Order)에 따라 결정되었기 때문이다. 파이썬이 상속 그래프를 조회할 때는 특정한 순서를 따르는데 이 것이 바로 MRO, 메서드 결정 순서이다. 클래스에 있는 __mro__ 속성은 현재 클래스부터 object 클래스까지 슈퍼클래스들의 MRO를 튜플 형태로 저장한다. 값을 확인하기 위해서는 __mro__를 호출하면 된다. 


D.__mro__의 결과는 (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>) 이와 같이 나온다. (다중 상속할 때, B와 C의 순서를 바꾸면 결과 값 역시 B와 C가 달라진다.)


MRO 리스트 자체를 실제로 결정할 때는 C3 선형화라는 기술을 사용한다. 너무 계산이 복잡해지지 않도록 부모 클래스의 MRO를 다음 세 가지 제약 조건 하에서 합병 정렬 한다.

  • 자식 클래스를 부모보다 먼저 확인한다.
  • 부모 클래스가 둘 이상이면 리스팅한 순서대로 확인한다.
  • 유효한 후보가 두가지 있으면 첫번째 부모 클래스부터 선택한다.

위에서 상속 시 부모 클래스의 메서드를 호출하기 보다 super()를 사용하라고 명시하였는데 그 이유는 아래와 같다.

class Pet(object):
    def __init__(self):
        print("pet")


class Lion(Pet):
    def __init__(self):
        Pet.__init__(self)
        print("lion")


class Tiger(Pet):
    def __init__(self):
        Pet.__init__(self)
        print("tiger")


class Liger(Lion, Tiger):
    def __init__(self):
        Lion.__init__(self)
        Tiger.__init__(self)
        print("liger")


if __name__ == '__main__':
    liger = Liger()

결과를 출력하면 pet이 두번 나오는 것을 확인할 수 있다. 크게 문제되지 않을 것 같지만 예상했던 결과가 아니니, __init__을 super()로 바꾸어주는 게 좋다.


위와 같은 결과가 나오는 이유인 즉슨 super() 함수를 사용할 때, 파이썬은 MRO의 다음 클래스에서 검색을 시작한다. 재정의한 모든 메서드가 모두 super()를 사용하고 한 번만 호출하지만, 시스템은 MRO 리스트 전체에 동작하고 모든 메서드는 한 번만 호출한다. 바로 이 때문에 super를 사용하면 pet의 생성자가 두 번 호출되지 않는 것이다.


그러나 super()가 항상 정답은 아니다. 원치 않은 메서드가 호출될 수도 있기 때문이다. 그래서 python cookbook의 저자는 다음과 같은 규칙을 따르는 것이 좋다고 하였다.


  1. 상속 관계에서 이름이 같은 모든 메서드는 동일한 구조를 가지게 해야 한다.
  2. 가장 상위에 있는 클래스에서 메서드 구현을 제공해, MRO에서 검색을 할 때 결국은 실제 메서드에서 멈추도록 설계 해야한다.


참고

http://www.jesshamrick.com/2011/05/18/an-introduction-to-classes-and-inheritance-in-python/

http://stackoverflow.com/questions/576169/understanding-python-super-with-init-methods

전문가를 위한 파이썬

Python Cookbook


Comments