사람 ⊂ 동물 ⊂ 생물

어떤 범주는 다른 범주에 포함되는 부분집합이 될 수 있다. 예를 들어, 사람은 동물에 포함되는 하위 범주이고, 생물은 동물을 포함하는 상위 범주다. 정보를 범주로 다룰 때, 이런 식의 계층적 분류는 어디서나 볼 수 있다. 물론 프로그래밍에서 다루는 데이터에도 이런 형태가 나타날 것이다. 예를 들어, ‘삼각형’과 ‘사각형’이라는 데이터 유형은 ‘도형’이라는 더 넓은 데이터 유형에 속한다고 할 수 있다.

범주에 포함 관계가 있다면, 범주를 나타내는 클래스에도 포함 관계가 있을 것이다. 파이썬에서는 클래스를 다른 클래스에 포함되는 하위 클래스로 정의할 수 있다. 이 절에서는 하위 클래스를 정의하는 방법을 배우고, 클래스의 포함 관계에서 나타나는 특징을 알아본다.

8.4.1 하위 클래스 정의하기

클래스를 정의할 때 그 클래스가 포함될 상위 클래스를 지정할 수 있다. 헤더 행에서 클래스 이름 뒤에 괄호를 붙이고, 괄호 속에 상위 클래스로 삼을 클래스를 표기하면 된다.

class B(A):
    """A 클래스의 하위 클래스인 B 클래스"""
    이 하위 클래스의 공용 속성

책이라는 범주는 종이책, 전자책, 죽간 등의 하위 범주로 세분화해 볼 수 있다. 다음은 Book 클래스의 하위 클래스로 죽간을 나타내는 BambooBook 클래스를 정의해 본 것이다.

코드 8-39 ‘책’ 클래스를 상위 클래스로 삼는 ‘죽간’ 클래스 정의하기

>>> class BambooBook(Book):
...     """죽간을 나타내는 클래스. Book의 하위 클래스다."""
...     origin = '중국'
...     material = '대나무'
... 

BambooBook 클래스 정의를 보면 괄호 안에 Book을 표기하여 이 클래스를 상위 클래스로 삼고 있다. 이 외에도 눈여겨 볼 사항이 몇 가지 있다.

  • 이 클래스 안에는 자신만의 속성 origin을 정의해 두었다. 이는 상위 클래스인 Book에는 없는 속성이다. 이처럼 상위 클래스에는 없는 속성을 하위 클래스에 추가로 정의할 수 있다.
  • 이 클래스 안에 정의된 material 속성은 상위 클래스인 Book에도 정의된 속성이다. BambooBook 클래스나 이 클래스의 인스턴스에서 material 속성을 읽으면 상위 클래스의 '종이'가 아니라 새로 정의한 '대나무'가 반환된다. 이처럼, 상위 클래스에 정의된 속성을 하위 클래스에서 새로 정의할 수 있다. 이를 재정의(override)라고 한다.
  • 하위 클래스에서 재정의하지 않은 상위 클래스의 속성은 하위 클래스에서 그대로 읽을 수 있다. 이 예에서는 인스턴스를 초기화하는 __init__() 메서드와 describe() 메서드는 재정의하지 않았다. 따라서 이 클래스를 인스턴스화하면 상위 클래스의 Book.__init__() 메서드를 통해 인스턴스의 초기화가 이루어지며, describe() 메서드를 호출할 경우 상위 클래스의 Book.describe() 메서드가 호출된다.

정의한 클래스를 확인해보고, 인스턴스화하여 테스트도 해 보자.

코드 8-40 BambooBook 클래스와 인스턴스의 동작

>>> BambooBook                # 정의된 클래스 객체
<class '__main__.BambooBook'>

>>> BambooBook.origin         # 추가한 클래스 속성
'중국'

>>> BambooBook.material       # 재정의한 클래스 속성
'대나무'

>>> bamboo_book_1 = BambooBook('묵자', '묵자', None)  # 인스턴스화
>>> bamboo_book_1.describe()  # 상위 클래스의 메서드 호출
묵자는 묵자 씨가 저술했다.
이 책은 대나무로 만들어졌다.

위 코드에서 알 수 있듯, 하위 클래스에서 새로 정의한 속성을 사용할 수 있으며, 재정의하지 않은 상위 클래스의 속성도 사용할 수 있다. 또한 describe() 메서드가 Book 클래스의 속성이지만 그 동작은 BambooBook 클래스와 인스턴스를 기준으로 이루어진다는 점도 잘 봐두자.

상속·확장: 하위 클래스 정의를 가리키는 다른 용어

어떤 클래스의 하위 클래스를 정의하는 것을 ‘상속(inherit)’ 또는 ‘확장(extend)’이라고 부르기도 한다. 다음은 사용되는 용어가 다르지만 모두 같은 뜻이다.

  • 상위 클래스 A의 하위 클래스 B를 정의한다.
  • A 클래스를 상속하는 B 클래스를 정의한다.
  • A 클래스를 확장하는 B 클래스를 정의한다.

‘상속’이라는 용어가 붙은 것은 하위 클래스가 상위 클래스의 속성을 공유하는 것이 마치 자식이 부모의 특성을 대물림하는 것과 비슷하기 때문이다.

  • 기능적 관점에서, 하위 클래스에는 상위 클래스의 모든 기능을 고스란히 가지면서 추가 기능도 가질 수 있기 때문에 상위 클래스를 ‘확장’하는 것이기도 하다.

__init__() 메서드 재정의하기

하나의 상위 범주에는 다양한 하위 범주가 속할 수 있다. Book 클래스의 또 다른 하위 객체로, 전자책을 나타내는 클래스 EBook을 정의해 보자. 이번에는 __init__() 메서드를 재정의하는 요령도 알아볼 것이다.

코드 8-41 __init__() 메서드 재정의하기

>>> class EBook(Book):
...     """전자책을 나타내는 클래스. Book의 하위 클래스다."""
...     material = '전자'
...     #
...     def __init__(self, title, author, pages, filetype):
...         """인스턴스를 초기화한다."""
...         # 이 클래스(하위 클래스)의 인스턴스의 전용 속성을 초기화한다
...         self.filetype = filetype   # 파일 형식
...         #
...         # 나머지 데이터를 상위 클래스의 초기화 메서드에 넘겨 초기화한다
...         Book.__init__(self, title, author, pages)
... 

재정의한 __init__() 메서드를 자세히 살펴보자. 이 메서드는 자신이 속한 클래스의 인스턴스 전용 속성(self.filetype)만을 전달받은 인자 중 일부(filetype)를 이용해 초기화하고, 나머지 속성은 상위 클래스의 Book.__init__() 메서드에 남은 인자(title, author, pages)를 전달하여 초기화한다. 이렇게 상위 클래스의 초기화 메서드를 활용하면 불필요한 코드 중복을 막을 수 있다. 물론, 이 메서드에서 인스턴스의 모든 속성을 직접 초기화할 수도 있다. 그러나 코드의 중복을 방지하고 상위 클래스와 하위 클래스의 일관성을 유지한다는 측면에서 이 방식이 더 유리하다.

전자책 클래스의 인스턴스를 생성하여 초기화 결과를 확인해보자.

코드 8-42 하위 클래스의 인스턴스

>>> ebook_1 = EBook('손에 잡히는 Vim', '김선영', 180, 'pdf')
>>> ebook_1.filetype    # 하위 클래스의 인스턴스 전용 속성
'pdf'

>>> ebook_1.describe()  # 상위 클래스의 메서드 호출
손에 잡히는 Vim은 김선영 씨가 저술했다.
이 책은 전자로 만들어졌다.

하위 클래스의 인스턴스 전용 속성 filetype과 상위 클래스에서 상속받은 인스턴스 전용 속성 title, author, pages가 모두 잘 초기화되어 있다.

8.4.2 클래스의 계층 살펴보기

“사람 ⊂ 동물 ⊂ 생물”의 예처럼, 범주는 여러 단계의 계층을 형성할 수 있다. 이는 하위 클래스를 상속하는 클래스를 연쇄적으로 정의하여 표현할 수 있다. 다음은 Book 클래스를 상속하는 EBook 클래스를 상속하는 DownloadableEBook 클래스를 정의해 본 것이다.

코드 8-43 하위 클래스를 상속하는 클래스

>>> class DownloadableEBook(EBook):
...     """다운로드할 수 있는 전자책을 나타내는 클래스. EBook을 상속한다."""
...     def __init__(self, title, author, pages, filetype, url):
...         """인스턴스를 초기화한다."""
...         self.url = url   # 다운로드 URL
...         EBook.__init__(self, title, author, pages, filetype)
...

이를 통해 Book - EBook - DownloadableEBook 이라는 범주의 계층이 형성된다.

참고로 파이썬의 모든 클래스는 object 클래스의 하위 클래스다. 클래스를 정의할 때 다른 클래스를 상속하지 않으면 object 클래스를 상속하여 정의된다.

하위 클래스 확인하기

issubclass() 함수를 사용하면 어떤 클래스가 다른 클래스의 하위 클래스인지를 검사할 수 있다. 몇 가지 클래스를 대상으로 시험해 보자.

코드 8-44 issubclass() 함수로 하위 클래스 검사

>>> issubclass(BambooBook, Book)  # BambooBook이 Book의 하위 클래스인가?
True

>>> issubclass(EBook, Book)       # EBook이 Book의 하위 클래스인가?
True

>>> issubclass(Book, Book)        # Book이 Book의 하위 클래스인가?
True

>>> issubclass(int, Book)         # int가 Book의 하위 클래스인가?
False

BambooBookEBookBook의 하위 클래스이고, 전혀 다른 클래스인 intBook의 하위 클래스가 아니다. 흥미로운 점은 BookBook의 하위클래스라는 점이다. 이는 집합 이론에서 한 집합이 자신의 부분집합인 것과 같은 이치다.

모든 클래스는 object의 하위 클래스이므로 object를 대상으로 검사하면 항상 참이 반환된다.

코드 8-45 모든 클래스는 object의 하위 클래스다

>>> issubclass(object, object)    # object가 object의 하위 클래스인가?
True

>>> issubclass(int, object)       # int가 object의 하위 클래스인가?
True

>>> issubclass(Book, object)      # Book이 object의 하위 클래스인가?
True

>>> issubclass(EBook, object)     # EBook이 object의 하위 클래스인가?
True

>>> issubclass(DownloadableEBook, object)
True

위 코드에서 알 수 있는 또 한가지 사실은 issubclass() 함수는 클래스가 다른 클래스를 직접 상속하지 않고 상위 클래스를 통해 간접적으로 상속하더라도 하위 클래스로 판단한다는 점이다.

한편, 어떤 클래스를 직접 상속하는 클래스를 모두 구하고 싶다면 클래스에 자동으로 정의되어 있는 __subclasses__() 메서드를 활용하면 된다.

코드 8-46 Book을 직접 상속하는 클래스 구하기

>>> Book.__subclasses__()
[<class '__main__.BambooBook'>, <class '__main__.EBook'>]

__subclasses__() 메서드는 자신을 직접 상속하는 클래스만을 반환한다. 자기자신(Book)이나 자신을 간접적으로 상속하는 클래스(DownloadableEBook)는 결과에서 제외된다.

클래스 계층의 이름공간

하위 클래스는 상위 클래스의 이름공간을 공유하는 동시에, 자신만의 이름공간도 갖는다. 반면 상위 클래스는 하위 클래스의 이름공간에 접근하지 못한다. 다음 예를 보자.

코드 8-47 상위 클래스와 하위 클래스의 이름공간 비교

>>> Book.publisher = '인사이트'  # Book 클래스에 속성 추가
>>> EBook.publisher              # 하위 클래스는 상위 클래스의 속성에 접근 가능
'인사이트'

>>> DownloadableEBook.publisher
'인사이트'

>>> EBook.publisher = 'insight'  # 하위 클래스에서 속성 재정의
>>> Book.publisher               # 상위 클래스의 속성은 그대로 유지된 채
'인사이트'

>>> EBook.publisher              # 하위 클래스의 속성만 변경되었다
'insight'

>>> DownloadableEBook.publisher
'insight'

>>> EBook.filetypes = ('pdf', 'epub', 'mobi')  # 하위 클래스에 속성 추가 
>>> DownloadableEBook.filetypes  # 하위 클래스에서는 접근 가능
('pdf', 'epub', 'mobi')

>>> Book.filetypes               # 상위 클래스에서는 접근 불가
AttributeError: type object 'Book' has no attribute 'filetypes'

상위 클래스와 하위 클래스의 이름공간의 관계는 마치 클래스와 인스턴스의 이름공간의 관계와 유사하다. 클래스의 계층과 인스턴스에 따른 이름공간의 범위를 그림으로 보면 좀 더 이해하기 쉬울 것이다.

그림 8-4 상위 클래스, 하위 클래스, 인스턴스의 이름공간의 범위 (준비중)

8.4.3 다중 상속

다중 상속이란 클래스가 두 개 이상의 상위 클래스를 나란히 상속하는 것이다. 휴대전화 클래스가 전화 클래스와 휴대용품 클래스를 동시에 상속하는 것을 예로 들 수 있다. 하지만 전자제품 클래스를 상속하는 전화 클래스를 상속하는 휴대전화 클래스처럼, 단순히 수직적 관계로 여러 개의 클래스를 상속하는 경우는 다중 상속이라고 하지 않는다.

그림 8-5 다중 상속 (준비중)

클래스가 여러 개의 상위 클래스를 상속하도록 하려면 괄호 안에 상속할 클래스의 이름을 콤마로 구분하여 나열하면 된다.

코드 8-48 휴대전화 클래스의 다중 상속

>>> class Phone:
...     """전화"""
...     description = '전기를 이용한 원거리 의사소통 도구'
...     
...     def call(number):
...         print(number, '에 전화를 겁니다.')
...         # 실제로 전화 거는 동작은 생략 ...
... 
>>> class PortableItem:
...     """휴대용품"""
...     description = '휴대할 수 있는 도구'
...     is_portable = True
... 
>>> class PortablePhone(Phone, PortableItem):  # 상속할 클래스를 괄호 안에 나열
...     """휴대전화"""
...     has_battery = True
... 

하위 클래스는 자신이 상속한 상위 클래스들의 속성을 읽을 수 있다. 이 때, 상속한 상위 클래스의 나열 순서에 의미가 있다. 알다시피 하위 클래스에 속성이 없을 때는 상위 클래스의 이름공간에서 속성을 찾는다. 이 때 상위 클래스가 여러 개인 경우 왼쪽에 나열한 클래스의 이름공간을 먼저 검색한다. 다음 코드를 보자.

코드 8-49 다중 상속한 클래스의 이름공간 검색

>>> PortablePhone.call('01012345678')  # Phone의 속성(메서드)
01012345678 에 전화를 겁니다.

>>> PortablePhone.is_portable          # PortableItem의 속성
True

>>> PortablePhone.description          # Phone의 속성
'전기를 이용한 원거리 의사소통 도구'

description 속성은 Phone 클래스와 Portable 클래스 둘 다에 정의되어 있지만 class PortablePhone(Phone, PortableItem): 에서 더 왼쪽에 나열된 Phone 클래스의 속성이 먼저 발견된다. 다중 상속한 클래스에서는 이름공간을 왼쪽 - 오른쪽 - 위쪽 순으로 검색한다. 그림 8-6을 참고하자.

그림 8-6 다중 상속한 클래스의 이름공간 검색 (준비중)

보다시피 다중 상속은 이름공간의 검색 순서가 복잡하여 혼동을 일으킬 우려가 있다. 여기서 예로 든 것은 비교적 단순한 경우다. 대규모 프로젝트에서는 다중상속의 대상이 되는 상위 클래스도 다른 클래스를 다중상속하는 등 훨씬 복잡해질 수 있다. 이런 문제 때문에 다중 상속을 남용하는 것은 바람직하지 않다. 자바·루비처럼 다중 상속을 금지하는 언어도 있다. 파이썬에서는 다중 상속이 허용되지만 주의 깊게 사용해야 한다.

믹스인

클래스 중에는 멤버 변수 없이 모든 속성이 메서드로만 이루어진 특별한 클래스가 있는데, 믹스인(mixin)이라고 부른다. 믹스인은 다양한 클래스에서 공통적으로 사용할 수 있는 메서드들을 묶어둔 클래스다. 클래스를 정의할 때 필요한 믹스인을 다중 상속하여 섞어 넣어(mix in), 믹스인의 메서드를 이용할 수 있다. 믹스인에는 변화하는 상태가 존재하지 않고 오직 메서드만 있기 때문에 다중 상속을 하더라도 비교적 안전하다.

연습문제

연습문제 8-10 도형 클래스 정의하기 1

다음 요구사항을 참고해 도형(Shape), 삼각형(Triangle), 사각형(Rectangle)을 나타내는 클래스를 정의하여라.

  • Shape: 도형을 나타내는 클래스
    • 클래스 메서드 describe(): “이 도형은 3 개의 변을 갖고 있습니다.”와 같이 이 도형의 특징을 화면에 출력한다. 변의 개수는 self.sides 속성을 읽어 구한다.
  • Triangle: 삼각형을 나타내는 클래스
    • Shape 클래스를 상속
    • 클래스 속성 sides: 변의 개수를 나타내는 속성. 3으로 고정
  • Rectangle: 사각형을 나타내는 클래스
    • Shape 클래스를 상속
    • 클래스 속성 sides: 변의 개수를 나타내는 속성. 4로 고정

클래스를 정의한 후에는 프로그램 하단에 도형의 인스턴스를 만들어 특징을 출력하는 다음 코드를 삽입하여라.

shapes = [
    Triangle(),
    Rectangle(),
]
for shape in shapes:
    shape.describe()

프로그램을 실행한 결과는 다음과 같아야 한다.

이 도형은 3 개의 변을 갖고 있습니다.
이 도형은 4 개의 변을 갖고 있습니다.

연습문제 8-11 도형 클래스 정의하기 2

아래 사항을 참고해 연습문제 8-10에서 정의한 도형 클래스에 도형의 각 좌표 속성과 둘레 계산 메서드를 추가하여라. 이 때, Coordinate 클래스는 연습문제 8-9에서 정의한 것을 활용하도록 한다.

  • Triangle 클래스에 추가할 속성
    • 클래스 메서드 circumference(): 이 삼각형의 둘레를 계산하여 반환한다.
    • 인스턴스 속성 point_a: 꼭지점 A의 좌표. (Coordinate 유형)
    • 인스턴스 속성 point_b: 꼭지점 B의 좌표. (Coordinate 유형)
    • 인스턴스 속성 point_c: 꼭지점 C의 좌표. (Coordinate 유형)
  • Rectangle 클래스에 추가할 속성
    • 클래스 메서드 circumference(): 이 사각형의 둘레를 계산하여 반환한다.
    • 인스턴스 속성 point_a: 꼭지점 A의 좌표. (Coordinate 유형)
    • 인스턴스 속성 point_b: 꼭지점 B의 좌표. (Coordinate 유형)
    • 인스턴스 속성 point_c: 꼭지점 C의 좌표. (Coordinate 유형)
    • 인스턴스 속성 point_d: 꼭지점 D의 좌표. (Coordinate 유형)

클래스를 정의한 후에는 프로그램 하단에 도형의 인스턴스를 만들어 특징과 둘레를 출력하는 다음 코드를 삽입하여라.

shapes = [
    Triangle(Coordinate(0, 0), Coordinate(3, 0), Coordinate(3, 4)),
    Rectangle(Coordinate(2, 2), Coordinate(6, 2), Coordinate(6, 6), Coordinate(2, 6)),
]
for shape in shapes:
    shape.describe()
    print('둘레:', shape.circumference())

프로그램을 실행한 결과는 다음과 같아야 한다.

이 도형은 3 개의 변을 갖고 있습니다.
둘레: 12.0
이 도형은 4 개의 변을 갖고 있습니다.
둘레: 16.0

힌트: Coordinate 클래스의 거리 계산 메서드 distance()를 활용해라.

힌트: 인스턴스의 속성은 초기화 메서드 __init__()를 이용해 정의해라.