[python] python3 힘들게 배우기 #19

9 분 소요

ex44. 상속 vs. 합성

동화 속에는 언제나 어두운 숲 또는 그에 상응하는 공간이 등장한다. 주인공이 고난을 겪을 걸 알면서도 그 곳에 가지 않는 이야기는 등장하지 않는다.

프로그래머들도 마찬가지다. ‘상속’ 어두운 숲에 빠져들기 쉽고 ‘Complexity’라는 여왕에게 잡아먹히기 쉽다. 상속에 대한 재미있는 비유다.

현재 숲속에서 싸움을 벌이는 프로그래머들은, 아마 당신에게 당신도 들어와야 한다고 말할 것이다. 왜냐 하면 그들이 다루기에 너무 많은 것들을 만들었기 때문에 당신의 도움이 필요하기 떄문이다. 하지만 당신은 항상 이 사실을 염두에 두어야 한다:

대부분의 상속은, 조합으로 대체되거나 간단하게 바꿀 수 있다. 그리고 중복 상속은 어떤 식으로든지간에 피해야 한다.

상속(Inheritance)이란 무엇인가?

상속은 하나의 클래스가 그 자신의 특질의 대부분이나 모든 부분을 부모 클래스로부터 받는 것을 말한다. 이를테면 당신이 ‘Bar클래스의 상속을 받는 Foo클래스를 만들어라’인 class Foo(Bar)라고 선언하는 것을 뜻한다. 이를 통해서 공통 함수를 Bar클래스에, 특별한 함수를 Foo클래스에 선언할 수 있다.

이런 특별화를 할 때, 부모클래스와 자식클래스 간에 3가지 interact방식이 있다.

  1. 자식 액션이 부모 액션을 내포함
  2. 자식 액션이 부모 액션을 덮어씀
  3. 자식 액션이 부모 액션을 대체함

하나씩 예시를 보도록 하자.

묵시적 상속(Implicit)

첫번째: 묵시적 상속 - 부모에는 있지만 자식에는 없는 함수

class Parent(object):

    def implicit(self):
        print("PARENT IMPLICIT()")

class Child(Parent):
    pass

dad = Parent()
son = Child()

dad.implicit()
son.implicit()

Child 클래스에 있는 pass는 빈 블럭을 만들고 싶을 때 사용하는 것이다. 이를 통해 Child클래스는 만들지만, 그 안에는 내용이 없다. 대신에 Parent클래스의 모든 것을 상속 받는다. 위 코드를 실행하면 아래와 같다.

PARENT IMPLICIT()
PARENT IMPLICIT()

child 클래스 안에 아무것도 없음에도 불구하고, son.implicit()함수가 작동하는 것에 주목하자. 해당 함수는 Parent클래스의 것을 호출하는 것이다. 이것은 시사하는 것은 당신이 만약 base class(i.e. Parent)에 함수를 넣으면, 모든 subclass들은(i.e. child)해당 함수를 *자동적으로* 갖게 된다는 것이다. 다양한 클래스에 대표적인 코드가 필요할 때 유용하게 사용할 수 있다.

명시적 덮어쓰기 (Override Explicitly)

암시적 함수사용의 문제점은, 자식함수에서는 다른 방식으로 함수가 작동하길 원할 때이다. 이럴 경우에 자식함수에 한번 더 함수를 선언하여, 효과적으로 그 함수를 대체할 수 있다. 이를 위해서는 한번 더 같은 이름의 함수를 선언하기만 하면 된다.

class Parent(object):

    def override(self):
        print("Parent override()")

class Child(Parent):

    def override(self):
        print("Child override()")

dad = Parent()
son = Child()

dad.override()
son.override()

실행시키면?

Parent override()
Child override()

대체 하기 전과 후!

대체(Alter)는 덮어쓰기(override)와 비슷한 것 같으면서도 약간 다른데.. override의 특별케이스라고 생각해도 좋을 것 같다. 먼저 덮어쓰기와 같이 똑같은 함수를 child 클래스에도 선언하고, 파이썬의 빌트인 함수인 super함수를 사용해서 Parent 클래스의 함수를 호출해 보도록 하자.

class Parent(object):

    def altered(self):
        print("PARENT altered()")

class Child(Parent):

    def altered(self):
        print("CHILD Before PARENT altered()")
        super(Child, self).altered() # alter()함수는 부모 것을 쓰자 는 의미
        print("CHILD After PARENT altered()")

dad = Parent()
son = Child()

dad.altered()
son.altered()

중요한 부분은 Child클래스의 altered()함수 내부이다. son.altered()가 실행되는 순간,

  1. Parent.alteredChild.altered로 덮어썼(ovverride)기 떄문에, 두 altered 함수 중 자식 함수가 선택되어 실행된다.
  2. 이 경우 print("CHILD Before PARENT altered()")가 실행된 후, 다음 줄에 다다르면 super(Child, self), 즉 Child클래스의 상위 클래스로 가서, .altered() altered함수를 실행하라는 명령에 다다른다. 다른 말로 Child와 self를 인수로 갖는 super함수를 호출하고 그에 딸린 altered()함수를 호출하라 로 읽을 수 있어야 한다.
  3. 이 시점에서 Parent.altered함수가 호출되고 실행된다.
  4. 마지막으로, 맨 마지막 줄인 print("CHILD After PARENT altered()") 가 실행된다.

직접 실행시키면, 아래와 같이 나온다.

PARENT altered()
CHILD Before PARENT altered()
PARENT altered()
CHILD After PARENT altered()

위 세 가지 경우의 수를 한 번에 코드로 나타내 보자.

class Parent(object):

    def implicit(self):
        print("Parent implicit()")

    def override(self):
        print("Parent override()")

    def altered(self):
        print("Parent override()")

class Child(Parent):

    def override(self):
        print("Child override()")

    def altered(self):
        print("Child before altered")
        super(Child, self).altered()
        print("Child after altered")

dad = Parent()
son = Child()

dad.implicit()
son.implicit()

dad.override()
son.override()

dad.altered()
son.altered()

실행하면

Parent implicit()
Parent implicit()
Parent override()
Child override()
Parent override()
Child before altered
Parent override()
Child after altered

Super()를 쓰는 이유

중복 상속(Multiple Inheritance)에 대해 파고들기 시작하면 골치아파지게 마련이다. 중복상속은 하나 이상의 클래스들로부터 상속받는 클래스를 정의할 떄를 의미한다. 이를테면,

class SuperFun(Child, BadStuff):
    pass

이 것이 의미하는 것은, “Child와 BadStuff클래스를 동시에 상속받는 SuperFun클래스를 만들어라” 라는 뜻이다.

이 경우에 Child와 BadStuff로부터 암묵적 상속을 받는 함수를 갖고 있으면, 파이썬은 그 함수를 실행할 때마다 두 개의 부모클래스를 전부 훑어봐야만 하고, 이는 일정한 순서를 필요로 한다. 파이썬에서는 이것을 하기 위한 순서를 “method resolution order”(MRO)라고 하는, C3라고 불리우는 알고리즘을 쓴다.

MRO가 복잡하고 잘 짜여진 알고리즘이기 때문에, 파이썬이 그것을 당신에게 넘기는 게 아니고 대신에 당신이 super()함수를 쓰도록 한다. super()만 있으면 순서에 대한 걱정 없이 파이썬이 올바른 함수를 찾아 준다.

init과 함께 사용하는 Super()

super()함수를 가장 빈번하게 사용하는 것은 __init__ 함수다.

class Child(Parent):

    def __init__(self, stuff):
      self.stuff = stuff
      super(Child, self).__init()

이것은 앞전에 altered()함수를 사용할 때와 비슷하다.

합성(Composition)

상속은 편리하긴 하지만, 단순히 다른 클래스나 모듈을 이용 함으로써 완벽히 똑같은 작업을 수행할 수 있는 방법이 있다. init을 통해 다른 클래스를 인스턴스화하는 것이다.

class Other(object):

    def implicit(self):
        print("Other implicit")

    def override(self):
        print("Other  override")

    def altered(self):
        print("Other altered")

class Child(object): 

    # init함수를 실행하는 게 상속과의 차이점. 
    def __init__(self): 
        # other라는 instance variable을 만들어서, Other클래스의 인스턴스로 선언.
        self.other = Other() 

    def implicit(self):
        self.other.implicit()

    def override(self):
        print("Child override")

    def altered(self):
        print("Child before Other altered")
        self.other.altered() # super도 쓸 필요없다. 인스턴스만 갖다 쓰면됨. 
        print("Child after Other altered")

son = Child()
son.implicit()
son.override()
son.altered()

실행시키면

Other implicit
Child override
Child before Other altered
Other altered
Child after Other altered

위에서 Other을 클래스로 선언해서 했던 것은 other.py파일을 작성해서 module로 실행해도 아무런 관계 없었던 작업이다.

언제 상속을 쓰고, 언제 조합을 써야 하는 것인가?

상속이냐 조합이냐의 의문은, 문제를 해결함에 있어 코드재사용에 대한 욕구 때문에 생긴다. 우리는 비효율적이고 깨끗하지 않기 때문에 코드가 중복되는 것을 바라지 않는다. 상속은 베이스 클래스에 기본적인 함수를 선언해 놓는 것으로, 조합은 모듈이나 다른 클래스에 함수를 선언해 놓고 사용하는 것으로 이를 해결한다.

두 가지 방법이 문제 해결에 동일하게 도움이 된다면, 어떤 게 적절한 방법일까? 이에 대한 대답은 믿을 수 없게 주관적이겠지만 나의 세 가지 가이드라인을 아래와 같이 제시해 보도록 한다.

  1. 중복된 상속을 절대로 만들지 말라. 에러가 생길 경우 클래스 상속과 에러를 찾는데 대단히 많은 시간이 들어간다.

  2. 코드를 패키지화하여 모듈로 만들고 조합을 통해 다양한 곳에 사용할 수 있도록 하라.

  3. 상속은 하나의 공통된 컨셉 이하에 다양하게 재사용될, 깨끗한 상하관계가 있을 때에만 사용하도록 하라.

위의 세 가지 룰에 꼭 목멜 필요는 없다. 단지 이 object-oriented programming이 남들과의 소통을 위한 작업임을 기억하라. 다른 사람들과의 작업에서 위 룰을 깨야만 한다면 그렇게 해도 좋다.

http://www.python.org/dev/peps/pep-0008/ 로 가서 파이썬 코딩 컨벤션에 대해 배워보자.

댓글남기기