17. 상속#
상속inheritance은 객체 지향 프로그래밍의 또 다른 주요 요소이다. 클래스를 선언할 때 다른 클래스의 속성과 메서드를 상속 받아 활용할 수 있다. 상속을 받는 클래스를 자식 클래스 또는 하위 클래스, 상속을 하는 클래스를 부모 클래스 또는 상위 클래스라고 부른다.
상속을 정의하는 방식은 다음과 같다.
class 자식클래스(부모클래스):
클래스 본문
17.1. 모음 자료형의 상속 체계#
아래 그림은 파이썬 모음 자료형의 상속 체계를 보여준다.
예를 들어, list
클래스는 Sequence
클래스를 상속하며,
Sequence
는 Collection
클래를 상속한다.
이렁 이유로 “리스트는 순차Sequence 자료형이다” 등으로 말한다.
이와 달리 항목들의 순서를 고려하지 않는 dict
와 set
은 순차 자료형이 아니다.
<그림 출처: 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. 연습문제#
참고: (실습) 상속