17. 상속#

상속inheritance은 객체 지향 프로그래밍의 또 다른 주요 요소이다. 클래스를 선언할 때 다른 클래스의 속성과 메서드를 상속 받아 활용할 수 있다. 상속을 받는 클래스를 자식 클래스 또는 하위 클래스, 상속을 하는 클래스를 부모 클래스 또는 상위 클래스라고 부른다.

상속을 정의하는 방식은 다음과 같다.

class 자식클래스(부모클래스):
    클래스 본문

17.1. 모음 자료형의 상속 체계#

아래 그림은 파이썬 모음 자료형의 상속 체계를 보여준다. 예를 들어, list 클래스는 Sequence 클래스를 상속하며, SequenceCollection 클래를 상속한다. 이렁 이유로 “리스트는 순차Sequence 자료형이다” 등으로 말한다. 이와 달리 항목들의 순서를 고려하지 않는 dictset 은 순차 자료형이 아니다.

<그림 출처: Problem Solving with Algorithms and Data Structures using Python의 1.13 절>

list, tuple, str 클래스 모두 Sequence 클래스를 상속하기에 인덱싱, 슬라이싱 등 자신들의 항목을 다루는 공통된 방식을 갖는다. 반면에 각 자료형마다 서로 다른 메서드를 제공한다. 이렇듯 한 클래스의 여러 자식 클래스는 서로 공통된 요소와 함께 각 자식 클래스 고유의 요소를 갖는다.

클래스를 상속할 때의 가장 큰 장점은 첫째, 기존에 작성된 코드를 필요에 따라 수정하고 재활용 할 수 있다는 것과 둘째, 자식 클래스의 인스턴스들 사이의 관계를 보다 잘 이해할 수 있다는 것이다.

두 가지 예제를 이용하여 상속을 설명한다.

17.2. Vector 클래스#

벡터는 정수 또는 부동소수점의 리스트를 의미하며 길이가 같은 두 벡터의 내적은 각 항목끼리의 곱셈의 합이다. 예를 들어 [2, 3, 4][5, 6, 9] 두 벡터의 내적은 다음과 같다.

2 * 5 + 3 * 6 + 4 * 9

리스트 클래스 list 는 벡터의 내적 연산자를 지원하지 않는다. 따라서 내적 연산을 지원하도록 list 클래스의 기능을 확장해야 한다. 아래 코드는 list 클래스를 상속하면서 벡터 내적 연산을 지원하는 Vector 클래스를 정의한다.

  • super().__init__(): 부모 클래스의 생성자 호출. 자식 클래스의 생성자를 호출할 때 호출되면 부모 클래스의 속성과 메서드를 모두 상속받음.

  • dot() 메서드: 추가되는 메서드. 두 벡터의 내적 반환.

  • len 속성: 추가되는 인스턴스 속성. 벡터의 길이.

# list 클래스 상속

class Vector(list):
    # Vector 클래스 생성자 재정의
    def __init__(self, items):
        """
        - list 클래스 상속
        - items: 벡터로 사용될 리스트
        """
        
        # 부모 클래스 생성자 호출
        super().__init__(items)
        
        # 속성 추가
        self.len = self.__len__()
            
    # 내적 메서드
    def dot(self, other):
        """
        벡터 내적
        """

        # 벡터의 길이가 다르면 실행 오류 발생
        if self.len != other.len:
            raise RuntimeError("두 벡터의 길이가 달라요!")

        # 내적 계산: 각 항목들의 곱의 합
        # 리스트를 상속하기에 인덱싱 사용 가능
        sum = 0
        for i in range(self.len):
            sum += self[i] * other[i]

        return sum

두 개의 벡터를 생성하자.

x = Vector([2, 3, 4])
y = Vector([5, 6, 9])

__str__() 등 리스트의 모든 매직 메서드와 append() 등의 다른 모든 메서드, 그리고 인덱싱, 슬라이싱 등 리스트의 모든 기능을 활용할 수 있다.

  • __str__() 메서드 지원

print(x)
[2, 3, 4]
  • 내적: 내적 연산도 잘 작동함

x.dot(y) # 2*5 + 3*6 + 4*9
64

17.3. 메서드 재정의#

append() 메서드도 잘 작동한다.

x.append(5)
x
[2, 3, 4, 5]

그런데 벡터에 포함된 항목의 수, 즉 len 속성의 값이 4로 변하지 않는다.

x.len
3

이유는 벡터의 항목이 변할 때 항목의 개수를 재확인하는 기능이 없기 때문이다. 따라서 시초에 한 번 확인된 항목의 수가 그대로 유지되는 문제가 발생한다.

이 문제를 해결하려면 append(), pop() 등 항목의 개수에 영향을 주는 메서드가 실행되면 자동으로 항목의 수를 조정하도록 해야 한다. 여기서는 예시를 위해 상속받은 append()pop() 두 메서드를 재정의 한다.

# list 클래스 상속

class Vector(list):
    # Vector 클래스 생성자 재정의
    def __init__(self, items):
        """
        - list 클래스 상속
        - items: 벡터로 사용될 리스트
        """
        
        # 부모 클래스 생성자 호출
        super().__init__(items)
        
        # 속성 추가
        self.len = self.__len__()
            
    # 내적 메서드
    def dot(self, other):
        """
        벡터 내적
        """

        # 벡터의 길이가 다르면 실행 오류 발생
        if self.len != other.len:
            raise RuntimeError("두 벡터의 길이가 달라요!")

        # 내적 계산: 각 항목들의 곱의 합
        # 리스트를 상속하기에 인덱싱 사용 가능
        sum = 0
        for i in range(self.len):
            sum += self[i] * other[i]

        return sum
    
    # append() 메서드 재정의
    def append(self, item):
        super().append(item)  # 부모 클래스의 append() 메서드 호출
        self.len += 1         # 벡터 길이 1 증가

    # pop() 메서드 재정의
    def pop(self, idx=-1):
        super().pop(idx)     # 부모 클래스의 pop() 메서드 호출
        self.len -= 1         # 벡터 길이 1 감소

다시 두 개의 벡터를 생성하자.

x = Vector([2, 3, 4])
y = Vector([5, 6, 9])
  • append() 메서드 (다시)

x.append(5)
x
[2, 3, 4, 5]

이제 벡터의 길이가 달라진게 확인된다.

x.len
4
  • pop() 메서드

x.pop()
x
[2, 3, 4]
x.pop(1)
x
[2, 4]

벡터의 길이도 달라진다.

x.len
2

재정의하지 않은 리스트의 다른 기능은 동일하게 작동한다.

  • 인덱싱

x[0]
2
  • 슬라이싱

x[::2]
[2]

벡터의 내적을 자주 활용한다면 함수로 지정하는 게 좋다. 아래 dot() 함수는 벡터 인자에 대해서만 작동하도록 구현되었다.

def dot(x, y):
    assert isinstance(x, Vector) and isinstance(y, Vector)
    
    return x.dot(y)
x.append(7)
x
[2, 4, 7]
dot(x, y) == x.dot(y)
True

17.4. 벡터 합#

길이가 동일한 두 벡터의 합(+)을 항목별 합으로 정의하려 한다. 그러려면 __add__() 매직 메서드를 재정의해야 한다.

# list 클래스 상속

class Vector(list):
    # Vector 클래스 생성자 재정의
    def __init__(self, items):
        """
        - list 클래스 상속
        - items: 벡터로 사용될 리스트
        """
        
        # 부모 클래스 생성자 호출
        super().__init__(items)
        
        # 속성 추가
        self.len = self.__len__()
            
    # 벡터 합 메서드 재정의
    def __add__(self, other):
        """
        벡터 합
        """

        # 벡터의 길이가 다르면 실행 오류 발생
        if self.len != other.len:
            raise RuntimeError("두 벡터의 길이가 달라요!")

        # 벡터 합 계산: 각 항목들의 합으로 이루어진 벡터
        new_list = []
        
        for i in range(self.len):
            item = self[i] + other[i]
            new_list.append(item)

        return Vector(new_list)

    # append() 메서드 재정의
    def append(self, item):
        super().append(item)  # 부모 클래스의 append() 메서드 호출
        self.len += 1         # 벡터 길이 1 증가

    # pop() 메서드 재정의
    def pop(self, idx=-1):
        super().pop(idx)     # 부모 클래스의 pop() 메서드 호출
        self.len -= 1         # 벡터 길이 1 감소

    
    # 내적 메서드
    def dot(self, other):
        """
        벡터 내적
        """

        # 벡터의 길이가 다르면 실행 오류 발생
        if self.len != other.len:
            raise RuntimeError("두 벡터의 길이가 달라요!")

        # 내적 계산: 각 항목들의 곱의 합
        # 리스트를 상속하기에 인덱싱 사용 가능
        sum = 0
        for i in range(self.len):
            sum += self[i] * other[i]

        return sum
    
# dot 함수도 새로 정의해야 함.
def dot(x, y):
    assert isinstance(x, Vector) and isinstance(y, Vector)
    
    return x.dot(y)

클래스를 수정하면 인스턴스를 새로 생성해야 변경된 내용이 반영된다.

x = Vector([2, 3, 4])
y = Vector([5, 6, 9])

벡터 내적은 동일하게 작동한다.

x.dot(y)
64
dot(x, y)
64

이제 벡터의 합이 원하는 대로 지원된다.

x + y
[7, 9, 13]

17.5. 연습문제#

참고: (실습) 상속