2.22. 상속

상속은 클래스가 다른 클래스를 확장 하게 해줍니다 – 그것의 모든 메서드와 속성으로 시작한 다음, 다른 부분을 추가하거나 재정의합니다. 원본을 기반(base) (또는 부모(parent))이라 부르고, 확장을 서브클래스(subclass) 라고 부릅니다.

기반 클래스 Shape가 서브클래스 Square를 가리키며 아래를 향합니다. Square는 하나의 메서드(area)를 재정의하고 나머지는 상속받습니다.

SquareShape 의 모든 메서드를 재사용하고 특수화가 필요한 것들만 재정의합니다.

2.22.1. 서브클래스 정의하기

기반 클래스는 class 줄의 괄호 안에 들어갑니다:

class Shape:
    def __init__(self, name):
        self.name = name

    def describe(self):
        return self.name + " with area " + str(self.area())

    def area(self):
        return 0

class Square(Shape):
    def __init__(self, side):
        super().__init__("square")
        self.side = side

    def area(self):
        return self.side * self.side

s = Square(4)
print(s.describe())

출력:

square with area 16

SquareShape 로부터 describe 를 변경 없이 상속하고 실제 값을 반환하도록 area 를 재정의합니다. 기반의 describeself.area() 를 호출하기 때문에 여전히 작동합니다 – 이는 실행 시점에 재정의로 해석됩니다.

2.22.2. super()

super() 는 메서드가 메서드의 기반 클래스 버전을 호출하게 해주는 프록시를 반환합니다. 가장 흔한 용도는 서브클래스의 __init__ 에서 기반의 __init__ 를 호출하여 기반이 자신의 속성을 초기화하도록 하는 것입니다:

class Square(Shape):
    def __init__(self, side):
        super().__init__("square")    # run Shape.__init__
        self.side = side

super().__init__ 를 잊는 것은 흔한 버그의 원인입니다: 기반이 설정했을 속성이 결코 설정되지 않고, 그것에 의존하는 상속된 메서드들이 나중에 충돌합니다.

2.22.2.1. 암묵적 기반 클래스

모든 클래스는 Python의 타입 계층 구조의 루트인 object 를 암묵적으로 상속합니다. __repr__, __eq__ 같은 메서드들과 속성 접근 뒤의 모든 메커니즘은 클래스가 기반을 선언하지 않더라도 거기서 옵니다. class Foo(object): 를 명시적으로 작성하는 것과 class Foo: 를 작성하는 것은 현대 Python에서 동등합니다. 후자가 관례적인 형태입니다.

2.22.3. 상속이 도움이 될 때 – 그리고 그렇지 않을 때

한 클래스가 진정으로 다른 클래스의 더 구체적인 종류 이고 대부분의 동작을 공유할 때 상속을 사용하세요. 고전적인 판별 기준은 “is-a”(이다) 검사입니다: SquareShape 이다.

두 클래스가 단지 몇 개의 헬퍼를 공유할 뿐이라면 상속은 과합니다. 대신 합성(composition) 을 사용하세요: 한 클래스가 다른 클래스의 인스턴스를 속성으로 보유하고 그 속성을 통해 사용하게 하세요. 형태는 “is-a”가 아니라 “has-a”(가지고 있다)입니다:

class Logger:
    def log(self, msg):
        print("[log]", msg)

# inheritance: Worker IS-A Logger
class WorkerInherits(Logger):
    def run(self):
        self.log("starting")

# composition: Worker HAS-A Logger
class WorkerComposes:
    def __init__(self):
        self.logger = Logger()

    def run(self):
        self.logger.log("starting")

둘 다 작동합니다. 합성 방식이 보통 더 나은데, 그 이유는:

  • 인터페이스를 작게 유지합니다. WorkerComposesrunlogger 만 노출합니다. WorkerInheritslog 도 노출합니다 – 의도했든 안 했든 호출자는 worker.log(...) 를 직접 작성할 수 있습니다.

  • 생명주기를 분리합니다. logger는 Worker 클래스를 건드리지 않고도 교체되거나, 워커들 간에 공유되거나, 워커마다 다르게 구성될 수 있습니다. 서브클래스는 그 기반에 단단히 묶여 있습니다.

  • 메서드 이름 충돌을 피합니다. 둘 다 run 을 정의하는 두 기반 클래스는 클래스가 둘 다로부터 상속하려 할 때 충돌합니다. 두 속성은 결코 충돌하지 않습니다.

실용적인 경험 법칙:

  • 서브클래스가 정말로 기반의 한 종류 이고 기반의 모든 메서드가 그것에도 의미가 있을 때 상속을 사용하세요 – SquareShape 이다; MemoryErrorException 이다.

  • 그 외 모든 경우에는 합성을 사용하세요 – 다른 클래스의 정체성이 아니라 동작 만 원할 때입니다. 대부분의 실제 코드는 상속보다 합성을 훨씬 더 많이 사용합니다.