8.1 데이터를 분류하는 방법에서 데이터 유형을 정의해야 하는 필요성과 방법을 알아보았다. 하지만 이 때 사용한 방법은 주석, 일반 함수, 사전 등을 이용한 비공식적인 방법이어서 type() 함수와 같은 프로그래밍 언어의 공식적인 지원을 받기 어려웠다. 그리고 8.2 클래스와 객체에서 파이썬에서 데이터 유형을 관리하는 공식적인 도구인 클래스를 알아 보았고, 데이터(객체)와 클래스의 관계에 대해서도 배웠다. 이제 배경지식은 다 배웠으니, 데이터 유형(클래스)을 파이썬의 공식 문법을 통해 직접 정의하는 방법을 알아보자.

8.3.1 class 문

클래스를 정의할 때는 class 문을 사용한다. class 문은 함수를 정의하는 def 문과 흡사한 형태다. 가장 간단한 형태의 클래스를 작성하는 양식을 먼저 살펴보고, 여기에 세부사항을 조금씩 덧붙이기로 하자.

class 클래스이름:
    """문서 문자열 (생략 가능)"""
    클래스 공용 속성

헤더 행과 본문 블록으로 구성되는 양식이 이제 꽤나 익숙할 것이다. class 문은 class 예약어로 시작된다. 그 뒤에 정의하려는 클래스의 이름을 작성하고 콜론으로 헤더 행을 마친다. 클래스 이름은 자유롭게 지을 수 있으나, 관례적으로 파스칼 표기법(PascalCase)으로 이름을 짓는다. 본문의 첫 행에는 함수의 문서 문자열과 마찬가지로 클래스의 용도를 설명하는 문서 문자열을 삽입할 수 있다. 그 아래에는 클래스에서 사용할 속성(attribute)을 여러 행에 걸쳐 입력할 수 있다. 속성이란 이 클래스가 가지는 데이터(멤버 변수)와 함수(메서드)를 말한다. 예를 들어, 책이라는 여기에 정의한 속성은 클래스의 모든 인스턴스가 공유한다.

이름 표기 관례

  • 파스칼 표기법: PythonProgramming처럼 단어와 단어를 대문자로 구별하는 방법이다. 파이썬 문화에서는 클래스 이름을 표기할 때 많이 사용된다.
  • 낙타 표기법(camelCase): pythonProgamming처럼 대문자로 단어를 구별하되 첫 단어는 소문자로 쓰는 방법이다. 파이썬 문화에서는 많이 사용되지 않는다.
  • 뱀 표기법(snake_case): python_programming처럼 단어와 단어를 밑줄 기호로 구별하는 방법이다. 파이썬 문화에서는 일반적인 변수의 이름으로 주로 사용된다. 변수에 저장된 데이터가 변경되지 않는 전역 데이터인 경우, 이 점을 강조하기 위해 PYTHON_PROGRAMMING처럼 모든 문자를 대문자로 표기하기도 한다.

다음은 이 양식을 이용해 책 데이터 유형을 나타내는 클래스를 정의해 본 예다.

코드 8-19 Book 클래스 정의하기

>>> class Book:
...     """책을 나타내는 클래스"""
...     # 클래스 공용 데이터
...     material = '종이'
... 
>>> Book     # Book이라는 이름으로 클래스가 생성되었다
<class '__main__.Book'>

클래스의 문서 문자열은 세 쌍따옴표로 """책을 나타내는 클래스"""라고 붙여 주었고, 그 아래에 클래스 공용 속성으로 material이라는 데이터를 하나 정의했다. 클래스를 정의한 후 클래스의 이름을 확인해 보면 클래스 객체(클래스는 type 클래스의 인스턴스다)가 생성되었음을 확인할 수 있다. Book이라는 이름 앞에 __main__이라는 게 붙어있는데, 이것은 이 클래스가 정의된 모듈을 의미한다. 모듈도 일종의 이름공간이다. 모듈에 관해서는 9장에서 알아 본다.

클래스를 정의했으니 인스턴스를 만들어 보자.

코드 8-20 Book의 인스턴스 만들기

>>> book_1 = Book()           # Book의 인스턴스 만들기
>>> book_2 = Book()           # 만드는 김에 하나 더 만들어 보자
>>> type(book_1)              # book_1의 데이터 유형 확인
<class '__main__.Book'>

>>> isinstance(book_2, Book)  # book_2가 Book의 인스턴스인지 확인
True

위 코드에서 보듯, 우리가 정의한 클래스도 Book()과 같은 표현으로 인스턴스화가 가능하다. 물론, 인스턴스를 원하는 만큼 여러 개 만들 수도 있다. 인스턴스의 데이터 유형을 type() 함수나 isinstance() 함수로 검사해 보면 역시 Book의 인스턴스가 맞다.

마치 파이썬을 설계한 사람이 int 같은 기본 데이터 유형을 정의해놓은 것처럼, 데이터 유형을 정의할 수 있게 되었다.

연습문제

연습문제 8-5 좌표쌍 클래스 정의하기

좌표쌍을 나타내는 클래스 Pair를 정의해 보아라. 이 클래스의 속성으로 xy를 정의하라. 두 속성의 값은 0으로 정의해 두면 된다.

지금 정의한 클래스는 이어지는 연습문제에서도 사용할 것이다.

8.3.2 속성과 이름공간

파이썬의 클래스는 일종의 이름공간(namespace)처럼 기능한다. 클래스 안에 정의해 둔 속성들은 클래스 안을 문맥(이름공간)으로 갖는 이름(변수)이라고 생각할 수 있다. 그런데 클래스 객체가 속성을 가질 수 있는 것처럼 인스턴스도 자신만의 속성을 가질 수 있다. 클래스의 속성은 모든 인스턴스가 공유하는 반면, 인스턴스의 속성은 개별 인스턴스만 사용할 수 있다. 이름공간의 접근 범위와 의미를 알아보자.

클래스 공용 속성

클래스의 속성은 클래스 객체 뿐 아니라 그 클래스의 모든 인스턴스가 공유한다. 앞에서 클래스 Book을 정의할 때 본문에 함께 정의해 두었던 material 속성을 읽어 보자. 클래스의 속성은 점 기호를 이용해 가리킬 수 있다.

코드 8-21 Book의 인스턴스 만들기

>>> Book.material    # Book 클래스의 material 속성 읽기
'종이'

>>> book_1.material  # book_1 객체의 material 속성 읽기
'종이'

>>> book_2.material  # book_2 객체의 material 속성 읽기
'종이'

보다시피 클래스와 인스턴스 모두가 material 속성에 접근할 수 있다. 한편, 클래스는 class 문으로 정의한 후에 클래스의 속성을 수정하는 것도 가능하다. 클래스의 인스턴스를 생성한 후 클래스의 속성을 수정했을 때도, 인스턴스에서 클래스의 속성을 읽을 수 있을까? 실험을 통해 확인해 보자.

코드 8-22 이미 정의한 클래스의 속성 수정하기

>>> Book.material = '대나무'     # 기존 속성 수정
>>> Book.publisher = '인사이트'  # 새 속성 추가
>>> Book.material                # 클래스의 속성이 수정되었다
'대나무'

>>> Book.publisher               # 클래스에 속성이 추가되었다
'인사이트'

>>> book_1.material              # 이전에 만든 인스턴스의 속성은?
'대나무'

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

>>> book_3 = Book()              # 인스턴스를 새로 만들면?
>>> book_3.material
'대나무'

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

이 실험에서 보듯, 클래스를 이미 정의한 뒤에도 속성을 수정할 수 있다. 클래스의 속성은 모든 인스턴스가 공유하므로, 클래스의 속성이 수정되면 인스턴스에서도 수정된 데이터를 읽을 수 있다.

클래스의 속성을 수정할 수 있긴 하지만 그것이 꼭 좋은 것은 아니다. 클래스의 속성은 모든 인스턴스가 믿고 의지하는 공용 데이터다. 그것이 마구 바뀐다면 혼란스럽지 않겠는가. 따라서 클래스 공용 데이터는 class 문 안에서 모두 정의해 두고, 꼭 그래야만 하는 특별한 경우가 아닌 한 수정하지 않는 것이 좋다. 클래스의 속성 중에 때때로 변경되어야 하는 것이 있다면, 그 데이터는 클래스보다는 인스턴스의 속성이어야 할 때가 대부분이다.

인스턴스 전용 속성

인스턴스는 클래스의 속성을 공유하는 한편 그와 별개로 자신만의 속성을 가질 수 있다. 이 속성은 클래스나 다른 인스턴스와 공유되지 않는 그 객체만의 전용 데이터다. 인스턴스에 속성을 추가하려면 인스턴스에 점 기호로 이름을 붙이고 데이터를 대입하면 된다.

코드 8-23 인스턴스에 속성 추가하기

>>> book_1.title = '손에 잡히는 Vim'  # book_1 객체에 속성 추가
>>> book_1.title    # 추가한 속성 확인
'손에 잡히는 Vim'

>>> Book.title      # 인스턴스에 추가한 속성은 클래스에는 없다
AttributeError: type object 'Book' has no attribute 'title'

>>> book_2.title    # 그 속성은 다른 인스턴스에도 없다
AttributeError: 'Book' object has no attribute 'title'

위 코드에서는 book_1 객체에 title 속성을 추가했다. 이 속성은 book_1 객체에서만 접근할 수 있고 Book 클래스나 다른 객체에서는 접근하지 못한다.

그런데 클래스 공용 속성을 인스턴스에서 수정하면 어떻게 될까?

코드 8-24 클래스에 존재하는 속성을 인스턴스에서 덮어썼을 때

>>> book_1.material  # Book 클래스의 material 속성을 가리키고 있다
'대나무'

>>> book_1.material = '석판'  # book_1 객체에 material 속성을 추가

>>> book_1.material  # 이제 자기자신의 material 속성을 가리킨다
'석판'
   
>>> Book.material    # 클래스는 계속 자신의 material 속성을 가리킨다
'대나무'

>>> book_2.material  # 다른 객체도 계속 클래스의 material 속성을 가리킨다
'대나무'

book_1 객체에서 material 속성을 읽으면 원래 Book 클래스의 공용 속성으로 평가되었다. 하지만 book_1.material = '대나무'를 실행하면 book_1 객체에 자기 전용의 material 속성이 추가되어 기존의 공용 속성은 가려진다. 클래스의 속성을 수정한 게 아니라는 점을 주의하자. book_1 객체는 이제 자신의 material 속성을 가리키지만, Book 객체와 다른 인스턴스(book_2)는 여전히 공용 속성을 가리킨다.

그림 8-2은 이 이름공간의 범위를 그림으로 나타낸 것이다. 그림을 보면 좀 더 이해하기 쉬울 것이다.

그림 8-2 이름공간의 범위 (준비중)

이름공간의 의미

‘화장실’이라는 이름은 문맥에 따라 다른 대상을 가리킨다. 음식점에서 “화장실”이 어디인가요? 라고 묻는 것과 집에서 “화장실 전구 좀 갈아줘.”라고 부탁할 때의 ‘화장실’은 이름이 같아도 서로 다른 대상을 가리킨다. 문맥 정보는 같은 이름으로도 다른 대상을 가리킬 수 있게 해 주어, 간결한 이름으로도 의사소통을 할 수 있게 한다. 문맥이 없다면 ‘우리은하 태양계 지구 대한민국 서울시 성북구 종암동 X-Y번지 3층 화장실’ 같은 식으로 긴 이름을 사용해야 할 것이다.

이름공간은 프로그래밍 언어에서 이름이 가리키는 대상을 제한하는 범위다. 클래스와 인스턴스는 이름공간이기 때문에, 그 안에서 짧고 간편한 이름을 사용하여 이름이 겹치더라도 대상을 구별할 수 있다.

앞의 예제에서는 material이라는 속성을 서로 다른 이름공간에서 정의했다. 이름공간을 잘 사용함으로써 이름을 Book_material, book_1_material처럼 길게 붙이지 않고도 정확한 대상을 지칭할 수 있다.

  • Book 클래스의 이름공간에 정의된 속성 material은 클래스와 전체 인스턴스의 공용 데이터를 가리킨다.
  • book_1 객체의 이름공간에 정의된 속성 material은 그 인스턴스만의 전용 데이터를 가리킨다.

한편, 이름에는 의미가 있어야 한다. 이름은 문맥에 의존적이므로 그 이름이 존재하는 문맥에서 어떤 의미를 가질지도 잘 생각할 필요가 있다. 앞의 예제의 material은 ‘재료’를 의미하는 영어 단어다. 그런데 그 이름이 사용된 문맥이 클래스냐 인스턴스냐에 따라 다음과 같이 다른 의미를 내포할 것이다.

  • Book 클래스의 이름공간에서는 책 일반의 재료를 의미한다.
  • book_1 객체의 이름공간에서는 그 책만의 고유한 재료를 의미한다.

문맥이라는 관점에서 보더라도, 클래스 이름공간은 모든 인스턴스가 공유하는 공통의 속성이 정의되는 공간인 셈이다.

dir() 함수로 이름공간에 정의된 이름 구하기

dir() 함수를 사용하면 이름공간(클래스)에 정의된 모든 이름(속성)의 리스트를 구할 수 있다. Book 클래스와 book_1 인스턴스에 정의된 이름을 확인해 보자.

코드 8-25 Book 클래스에 정의된 모든 이름 출력

>>> import pprint             # 출력되는 속성이 많으니 pprint를 사용하자
>>> pprint.pprint(dir(Book))  # Book 클래스의 모든 속성을 구해 출력
['__class__',
 '__delattr__',
 '__dict__',
 '__dir__',
 (...중략...)
 '__init__',
 (...중략...)
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 '__weakref__',
 'material']

dir() 함수로 Book 클래스의 속성을 구해 보았다. 직접 정의한 material 외에도 밑줄 두 개(__)로 시작하고 끝나는 속성이 많이 보인다. 이 속성들은 자동으로 정의된 것이며 저마다 용도가 다르다. 예를 들어, __doc__ 속성은 객체에 정의한 문서 문자열을 담고 있다. 이들 전체를 다루는 것은 이 책의 범위를 넘는 것이지만, 많은 수를 8.4.3 연산자의 동작 정의하기에서 알아볼 것이다.

파이썬의 기본 데이터 유형이나 다른 사람이 정의한 클래스에 어떤 속성이 있는지 확인하고 싶을 때 dir() 함수를 활용하자.

연습문제

연습문제 8-6 좌표쌍 클래스의 인스턴스 생성하기

연습문제 8-5에서 정의한 Pair() 클래스의 인스턴스를 두 개 생성하고, 인스턴스의 속성을 다음과 같이 각각 부여해라.

  • 첫번째 인스턴스(point_1): x축의 좌표는 -1, y축의 좌표는 2
  • 두번째 인스턴스(point_2): x축의 좌표는 2, y축의 좌표는 3

연습문제 8-7 좌표쌍 인스턴스의 거리 계산하기

연습문제 8-6에서 생성한 두 인스턴스의 거리를 계산하려 한다. 코드 8-2에서 정의한 두 점 사이의 거리를 계산하는 함수를 다음과 같이 정의하였다.

import math

def square(x):
    """전달받은 수의 제곱을 반환한다."""
    return x * x

def distance(point_a, point_b):
    """두 점 사이의 거리를 계산해 반환한다. (피타고라스의 정리)"""
    return math.sqrt(square(point_a['x'] - point_b['x']) +
                     square(point_a['y'] - point_b['y']))

이 함수를 참고해, Pair 인스턴스 두 개를 전달받아 거리를 계산하는 함수 pair_distance()를 정의해라.

다음은 이 함수를 이용해 인스턴스의 거리를 계산한 예다.

>>> pair_distance(point_1, point_2)
3.1622776601683795

힌트: 매핑(사전)에 저장된 값을 읽는 방법과 클래스에서 속성을 읽는 방법은 문법이 서로 다르다.

8.3.3 메서드

클래스와 인스턴스의 이름공간에는 다양한 데이터를 속성으로 정의할 수 있다. 그런데 함수도 데이터이므로, 클래스와 인스턴스의 속성이 될 수 있다. 이렇게 클래스나 인스턴스에 속한 함수는 그 데이터 종류를 위한 전용 함수로 기능하게 된다. 이 함수를 메서드(method)라고 부른다. 영어 단어 ‘method’는 ‘방법’이란는 뜻이다. 즉, 메서드는 데이터 유형을 다루는 방법이 정의된 함수다.

메서드 정의하기

메서드도 다른 속성과 마찬가지로 클래스 공용 속성으로 정의할 수도, 특정 인스턴스 전용 속성으로 정의할 수도 있다. 하지만 거의 대부분의 경우 메서드는 클래스 공용 속성으로 정의한다. 어떤 데이터 유형을 다루는 방법이 인스턴스마다 다른 경우란, 잘 없지 않겠는가. 따라서 메서드는 class 문 안에서 정의될 때가 많다. class 문 안에 함수를 정의하는 방법을 추가하면 class 문은 다음과 같은 형태가 된다.

class 클래스이름:
    """문서 문자열 (생략 가능)"""
    클래스 공용 속성
    
    def 메서드_1():
        """이 클래스를 다루는 함수 1..."""
        메서드의 본문
    
    ...(필요한 만큼 메서드를 추가 정의)

class 문 안에 def 문이 포함되었을 뿐, 어려울 것은 없다. 그러면 Book 클래스를 메서드를 추가한 버전으로 새로 정의해 보자.

코드 8-26 메서드 정의하기

>>> class Book:
...     """책을 나타내는 클래스"""
...     material = '종이'     # 클래스 공유 데이터
...     #
...     def describe():       # 클래스 공유 함수 (메서드)
...         """이 책에 관한 정보를 화면에 출력한다."""
...         print('이 책은 ' +  Book.material + '로 만들어졌다.')
... 

코드 8-26은 코드 8-19의 Book 클래스 정의에 describe() 메서드를 추가한 것이다. 함수가 클래스에 속성으로 포함되었다는 점을 빼면, 이해 못할 부분은 없을 것이다. 추가한 메서드는 material 속성을 읽어 책(클래스 또는 인스턴스)이 어떤 재질로 만들어졌는지를 화면에 출력하는 함수다. 단, 메서드는 별도의 이름공간을 가지므로 안에서 자신이 속한 클래스의 다른 속성을 가리키지 못한다. 따라서 material이라고만 쓰면 이름 오류가 발생한다. Book.material로 클래스 이름을 함께 명시해야 한다.

메서드는 다른 속성과 마찬가지로 점 기호를 이용해 가리킬 수 있다. 메서드를 호출하려면 여기에 괄호만 붙여주면 된다. 잘 되는지 한 번 실행해 보자.

>>> Book.describe()   # 메서드 호출
이 책은 종이로 만들어졌다.

코드 8-27 클래스 객체를 기준으로 메서드 호출

메서드가 의도한대로 잘 실행되는 것처럼 보인다. 그런데 코드 8-27은 Book 클래스 객체를 기준으로 메서드를 호출한 예다. 메서드를 클래스 공용 속성으로 정의했으므로, 클래스뿐 아니라 인스턴스에서도 실행할 수 있을 것이다. 그런데 이 메서드는 클래스 공용 속성인 Book.material을 읽어 출력하도록 정의해 두었다. 이 방법으로는 인스턴스 전용 속성을 출력하지는 못할 것이다. ‘이 책의 재료’를 출력하고 싶은데 ‘책 일반의 재료’만 출력하는 메서드가 된 것이다. 메서드가 인스턴스의 속성에 접근하도록 하려면 어떤 방법이 필요할까?

인스턴스를 위한 메서드

클래스가 아닌 인스턴스를 기준으로 describe() 메서드를 호출했을 때, 어떤 결과가 일어나는지 실험해 보자. Book 클래스의 인스턴스를 하나 만들고, 클래스 공유 속성과는 다른 값을 인스턴스 전용 속성으로 대입해 두자.

코드 8-28 클래스를 기준으로 메서드 실행하기

>>> book_1 = Book()  # 클래스를 새로 정의했으니 인스턴스도 새로 만들자
>>> book_1.material = '대나무'

인스턴스는 준비 됐다. 코드 8-27에서는 Book.describe()로 클래스를 기준으로 메서드를 호출했지만, 이번에는 book_1.describe()로 인스턴스를 기준으로 호출해 볼 것이다. 여기서 퀴즈! book_1.describe()의 실행 결과는 무엇일까? 한 번 예상해 보자.

  1. 이 책은 대나무로 만들어졌다가 출력된다.
  2. 이 책은 종이로 만들어졌다가 출력된다.
  3. 뭔가 오류가 발생할 것이다.

1번 결과가 나오면 좋겠지만, 메서드에서 Book.material을 출력하므로 합리적으로 생각하면 2번의 결과가 나올 것이라 예상할 수 있다. 하지만 실제로 실행해보면 엉뚱한 오류가 발생한다.

코드 8-29 인스턴스를 기준으로 메서드 호출

>>> book_1.describe()
TypeError: describe() takes 0 positional arguments but 1 was given

오류 메시지를 해석해보면, “describe()는 인자를 0개 전달받는 함수인데, 여기에 인자 1개가 전달되었다.”라는 의미다. 무슨 말일까? 우리는 분명, book_1.describe()를 실행하면서 인자를 하나도 전달하지 않았는데?

파이썬에서는 클래스를 기준으로 할 때와 인스턴스를 기준으로 할 때의 메서드 호출 방식이 다르다. 인스턴스를 기준으로 메서드를 호출할 때는 암묵적으로 메서드 호출의 기준이 되는 인스턴스가 첫번째 인자로 메서드에 전달된다. 그러므로 코드 8-29의 오류는 전달되는 데이터는 있는데 describe() 메서드에서 이를 전달받을 매개변수가 없어 발생한 것이다.

인스턴스 기준의 호출에서 인스턴스가 메서드에 인자로 전달되는 것은 인스턴스의 속성에 메서드가 접근할 수 있도록 하기 위한 것이다. 이를 활용해 describe() 메서드가 제대로 동작하도록 클래스를 새로 정의해 보자.

코드 8-30 인스턴스를 위한 메서드 정의하기

>>> class Book:
...     """책을 나타내는 클래스"""
...     # 클래스 공유 데이터
...     material = '종이'
...     #
...     # 클래스 공유 메서드
...     def describe(self):
...         """이 책에 관한 정보를 화면에 출력한다."""
...         print('이 책은 ' + self.material + '로 만들어졌다.')
... 

코드 8-30를 코드 8-26과 한 번 비교해 보자. 다른 부분은 동일한 데, describe() 메서드에 인스턴스를 전달받기 위한 매개변수 self가 추가되었다. 그리고 클래스의 속성을 가리키던 Book.material을 인스턴스의 속성을 가리키게끔 self.material로 변경하였다. ‘self’라는 이름은 ‘자신’을 의미하는 영어 단어에서 딴 것이다. 파이썬에서 인스턴스를 전달받는 메서드의 매개변수 이름으로 이 이름을 사용하는 것이 관례다.

클래스를 새로 정의했으니, 인스턴스를 새로 생성하여 메서드의 동작을 확인해보자.

코드 8-31 인스턴스를 위한 메서드 호출하기

>>> book_1 = Book()  # 클래스를 새로 정의했으니 인스턴스도 새로 만들자
>>> book_1.material = '대나무'
>>> book_1.describe()
이 책은 대나무로 만들어졌다.

이번에는 성공이다. 메서드는 인스턴스를 첫번째 매개변수(self)로 전달받아 이를 통해 인스턴스의 속성에 접근할 수 있다. 거의 대부분의 경우 메서드는 클래스가 아닌 개별 인스턴스를 조작하기 위한 함수다. 따라서 메서드의 첫번째 매개변수가 인스턴스를 전달받는 것으로 정의하는 것은 거의 메서드 정의의 기본 사항에 가깝다.

한편, 이와 같이 메서드를 인스턴스를 전달받도록 정의해 둔 상태에서는 클래스를 기준으로 메서드를 호출했을 때 오류가 발생한다. 메서드는 인스턴스를 전달받고 싶은데 클래스를 기준으로 함수를 호출했으므로 인스턴스가 전달되지 않기 때문이다. 이 때는 첫번째 매개변수로 클래스를 명시적으로 전달하면 된다. 다음 실험이 이를 보여준다.

코드 8-32 클래스 객체를 기준으로, 인스턴스를 위한 메서드 호출

>>> Book.describe()      # 클래스를 기준으로 메서드 호출: 오류
TypeError: describe() missing 1 required positional argument: 'self'

>>> Book.describe(Book)  # 인스턴스 대신 클래스를 전달하면 된다
이 책은 종이로 만들어졌다.

메서드는 여러 인스턴스가 공유하기 때문에 클래스의 속성으로 정의하는 경우가 대부분이다. 그런데 역설적이게도 메서드는 클래스가 아닌 인스턴스를 기준으로 실행해야 할 때가 많다. 파이썬 입문자는 이 점을 헷갈리기 쉽다. 실습을 통해 충분히 이해해두도록 하자. 메서드가 인스턴스에서 주로 활용되는 만큼 메서드의 첫번째 매개변수를 인스턴스를 의미하는 self로 하는 것은 거의 표준에 가까운 관습이다. 따라서 클래스 정의 규칙을 다음과 같이 정리해 두도록 하자.

class 클래스이름:
    """문서 문자열 (생략 가능)"""
    클래스 공용 속성
    
    def 메서드_1(self, ...):
        """이 클래스의 인스턴스(self)를 전달받아 처리하는 함수 1..."""
        메서드의 본문
    
    ...(필요한 만큼 메서드를 추가 정의)

정리해보자.

  • 메서드는 클래스와 인스턴스의 속성으로 정의된 함수이며, 특정 데이터 유형을 다루는 방법을 나타낸다.
  • 메서드는 대부분 class 문 내부에서 클래스 공용 속성으로 정의된다.
  • 메서드는 클래스 또는 인스턴스를 기준으로 호출할 수 있다. 인스턴스를 기준으로 호출할 때는 인스턴스가 메서드에 전달된다. 이를 통해 메서드에서 인스턴스 전용 속성에 접근할 수 있다.

연습문제

연습문제 8-8 좌표쌍 인스턴스의 거리를 메서드로 계산하기

연습문제 8-5에서 정의한 Pair 클래스를 새로 정의하여, 두 좌표쌍 사이의 거리를 계산해 반환하는 메서드 distance()를 정의해라. 이 메서드는 메서드 호출의 기준이 된 인스턴스와 거리를 계산할 다른 인스턴스를 각각 매개변수로 전달받아 거리를 계산한다.

클래스를 재정의한 뒤에는 연습문제 8-6의 인스턴스를 새로 생성하여 다음과 같이 메서드를 테스트해 보아라.

>>> point_1.distance(point_2)
3.1622776601683795

힌트: self 매개변수를 빼먹지 않도록 주의하자.

8.3.4 인스턴스의 초기화

클래스의 속성과 인스턴스의 속성은 역할과 의미가 다르다. 실제 프로그래밍에서는 메서드를 제외하면 클래스 공용 속성보다는 인스턴스 전용 속성을 정의하는 것이 더 중요하다. 인스턴스는 클래스의 실례이므로 그 객체마다 서로 다른 데이터를 가질 것이기 때문이다.

인스턴스를 새로 만들 때마다 그 속성을 정의해주어야 한다. 그렇지 않다면 같은 유형의 객체는 서로 구별되지 않을 것이며, 인스턴스를 새로 만드는 의미도 없을 것이다. 이렇게 객체를 생성한 직후 객체의 초기 속성을 정의하는 것을 초기화(initialization)라고 한다.

모든 객체는 생성 직후 초기화를 수행해야 한다. 그런데 인스턴스를 만들 때마다 속성을 하나씩 대입하는 것은 꽤나 불편한 일이다. 예를 들어, Book 클래스의 인스턴스를 3개 만들고 초기화한다고 해 보자.

코드 8-33 인스턴스의 속성을 일일이 대입하기가 번거롭다

>>> book_1 = Book()
>>> book_2 = Book()
>>> book_3 = Book()
>>> book_1.material = '종이'
>>> book_2.material = '대나무'
>>> book_3.material = '파피루스'

위 코드는 인스턴스화를 수행하는 단계와 인스턴스의 초기화 단계를 각각 나누어 수행하는데, 초기화할 속성이 하나 뿐인데도 코드가 길어지고 있다. 속성이 더 많다면 더욱 번거로운 작업이 될 것이고, 몇몇 속성을 대입하는 것을 잊어버릴 우려도 있다. 인스턴스화 과정에서 초기화까지 같이 수행할 수 있다면 편리하고 안전할 것이다.

__init__() 메서드로 인스턴스 초기화하기

클래스 이름에 괄호를 붙여 실행하면 인스턴스화가 수행된다는 점은 이미 알고 있다. 인스턴스화 과정은 크게 두 단계로 이루어진다.

  1. 클래스의 __new__() 메서드를 통해 새로운 객체를 만든다.
  2. 클래스의 __init__() 메서드를 통해 객체를 초기화한다.

__new__ 메서드와 __init__() 메서드는 우리가 직접 정의하지 않아도 클래스르를 정의할 때 자동으로 정의된다. 이 메서드를 호출하는 것 또한 프로그래머가 명시적으로 지시하지 않아도 인스턴스화 과정에서 자동으로 수행된다. 특히, __new__ 메서드는 거의 대부분의 경우 프로그래머가 건드릴 필요가 없다.

관심을 가져야 할 것은 인스턴스 초기화 메서드인 __init__()다. 기본 제공되는 __init__() 메서드는 객체에 아무런 속성도 부여하지 않는다. 하지만 이 메서드를 직접 정의하면 인스턴스의 초기화 방법을 지시할 수 있다. 다음은 코드 8-30의 Book 클래스에 __init__() 메서드를 추가한 것이다.

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

>>> class Book:
...     """책을 나타내는 클래스"""
...     # 클래스 공유 데이터
...     material = '종이'
...     #
...     # 클래스 공유 메서드
...     def __init__(self, material):
...         """인스턴스를 초기화한다."""
...         self.material = material
...     #
...     def describe(self):
...         """이 책에 관한 정보를 화면에 출력한다."""
...         print('이 책은 ' + self.material + '로 만들어졌다.')
... 

바뀐 부분은 __init`__ 메서드가 추가된 것 뿐이다. __init__() 메서드는 모든 메서드 중에서 첫번째 메서드로 정의할 때가 많지만, 반드시 따라야 하는 것은 아니다. 메서드의 매개변수를 보면, 생성된 인스턴스를 전달받는 self와 초기값으로 지정할 값을 전달받는 material이 있다. 메서드의 본문에서 materialself.material에 대입된다. 이를 통해 사용자가 지시한 값으로 인스턴스의 초기화를 수행할 수 있다. 새로 정의한 클래스를 인스턴스화해보자.

코드 8-35 인스턴스화 과정에서 초기화가 수행된다

>>> book_1 = Book('대나무')
>>> book_2 = Book('파피루스')
>>> book_1.material    # 인스턴스의 속성에 접근
'대나무'

>>> book_2.describe()  # 인스턴스의 속성에 접근하는 메서드
이 책은 파피루스로 만들어졌다.

인스턴스화를 수행할 때 Book('대나무')와 같이 초기화에 필요한 데이터를 괄호 안에 넣어 인자로 전달했다. __init__() 메서드는 첫번째 매개변수에 인스턴스를 전달받고, 두번째 매개변수부터 사용자가 전달한 인자를 입력받아 초기화에 사용한다. 그림으로 나타내면 다음과 같다.

그림 8-3 Book 클래스의 인스턴스화 과정 (준비중)

__init__() 메서드를 잘 정의해두면 인스턴스화 과정에서 초기화가 함께 수행되도록 할 수 있다. 그래서 인스턴스화가 필요한 거의 모든 클래스에서는 __init__() 메서드를 반드시 정의한다. 따라서 클래스 정의 규칙을 다음과 같이 좀더 확장하여 기억해두는 편이 좋다.

class 클래스이름:
    """문서 문자열 (생략 가능)"""
    클래스 공용 속성
    
    def __init__(self, ...):
        """인스턴스를 초기화한다."""
        인스턴스 전용 속성 초기화 규칙
    
    def 메서드_1(self, ...):
        """이 클래스의 인스턴스(self)를 전달받아 처리하는 함수 1..."""
        메서드의 본문
    
    ...(필요한 만큼 메서드를 추가 정의)

인스턴스가 가져야 할 속성

클래스 공용 속성으로는 메서드와 범주의 일반적 속성을 정의하는 것이 좋다. 반면에 인스턴스 전용 속성으로는 개별 객체의 고유한 속성을 정의해야 한다. __init__() 메서드는 인스턴스가 가져야 할 속성을 정하는 데 사용하기 좋다.

책은 단순히 종이를 재료로 한 물건만은 아니다. 개별 책에는 제목, 저자, 페이지 수 같은 속성을 정할 수 있다. 이를 반영해 Book 클래스를 수정해 보자.

코드 8-36 인스턴스가 가져야 할 속성을 __init__() 메서드에 정의

>>> class Book:
...     """책을 나타내는 클래스"""
...     # 클래스 공유 데이터
...     material = '종이'
...     #
...     # 클래스 공유 메서드
...     def __init__(self, title, author, pages):
...         """인스턴스를 초기화한다."""
...         self.title = title     # 책의 제목
...         self.author = author   # 책의 저자
...         self.pages = pages     # 페이지 수
...     #
...     def describe(self):
...         """이 책에 관한 정보를 화면에 출력한다."""
...         print(self.title + '은 ' + self.author + ' 씨가 저술했다.')
...         print('이 책은 ' + self.material + '로 만들어졌다.')
... 

위 코드에서는 개별 책 객체가 가져야 할 정보를 __init__() 메서드가 전달받아 초기화한다. title, author, pages라는 인스턴스 속성을 추가하고 그에 관한 설명을 주석으로 달아 두었다. 그에 맞춰 describe() 메서드도 객체의 속성을 출력하도록 수정했다. 이 때, describe 속성(메서드)은 개별 객체마다 가져야 할 정보가 아니므로 클래스 공용 속성으로 그대로 두었다. 이제 Book 클래스를 인스턴스화할 때 개별 책 데이터를 입력할 수 있다.

코드 8-37 개별적 정보가 입력된 인스턴스

>>> book_1 = Book('손에 잡히는 Vim', '김선영', 180)
>>> book_1.describe()
손에 잡히는 Vim은 김선영 씨가 저술했다.
이 책은 종이로 만들어졌다.

__init__() 메서드를 이용하면, 초기화에 필요한 정보가 누락되었을 때 인스턴스 생성이 방지되는 장점도 있다.

코드 8-38 필요한 속성이 누락될 경우 인스턴스 생성이 방지된다

>>> book_2 = Book('프로그래밍 클로저', '스튜어트 할로웨이')
TypeError: __init__() missing 1 required positional argument: 'pages'

개념 정리

  • 객체를 만들 때는 초기화가 필요하다.
  • __init__() 메서드를 통해 인스턴스의 초기화를 자동화할 수 있다.
  • __init__() 메서드에서 인스턴스 전용 속성을 정의해라.

연습문제

연습문제 8-9 좌표쌍 인스턴스 초기화하기

연습문제 8-8에서 정의한 Pair 클래스를 새로 정의하여, 인스턴스화 과정에서 속성 x와 속성 y를 초기화할 수 있도록 해 보아라. 인스턴스화할 때 초기값을 전달되지 않은 경우에는 기본값 0으로 초기화되도록 해라.

다음은 클래스를 새로 정의한 후에 인스턴스화를 수행하고 테스트하는 예다.

>>> point_1 = Pair(-1, 2)
>>> point_2 = Pair(y=3, x=2)
>>> point_3 = Pair()
>>> point_4 = Pair(10)
>>> point_1.x, point_1.y
(-1, 2)

>>> point_2.x, point_2.y
(2, 3)

>>> point_3.x, point_3.y
(0, 0)

>>> point_4.x, point_4.y
(10, 0)

>>> point_1.distance(point_2)
3.1622776601683795

8.3.5 상속

클래스를 이용하면 데이터를 범주화할 수 있다. 그런데 범주는 더 넓은 개념을 포괄하는 상위범주에 속할 수가 있고, 반대로 좀 더 좁은 개념을 나타내는 여러 하위범주를 구성할 수도 있다. 클래스는 상속(inheritance)을 이용해 이를 나타낼 수 있다. B 클래스가 A 클래스를 상속하면 B는 A의 하위 클래스, A는 B의 상위 클래스가 된다.

클래스 상속하기

A 클래스를 상속하는 B 클래스를 정의하려면 다음과 같이 헤더 행에서 클래스 이름 뒤의 괄호 속에 상속할 클래스를 표기하는 양식을 사용한다.

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 클래스와 인스턴스를 기준으로 이루어진다는 점도 잘 봐두자.

상속이라는 이름이 붙은 이유는 하위 클래스가 상위 클래스의 속성을 공유하는 것이 마치 자식이 부모의 특성을 상속하는 것과 흡사하기 때문이다. 상속을 활용하면 여러 클래스에서 반복적으로 정의되는 속성을 범주화하여 한 곳에 정의해 둠으로써 재사용을 꾀할 수 있다. 반복을 줄이면 그만큼 실수를 줄이고 작업 효율을 높일 수 있다.

__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가 모두 잘 초기화되어 있다.

클래스 계층

“생물 - 동물 - 사람” 처럼, 범주는 여러 단계의 계층을 형성할 수 있다. 이는 하위 클래스를 상속하는 클래스를 연쇄적으로 정의하여 표현할 수 있다. 다음은 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-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에서 정의한 도형 클래스에 도형의 각 좌표쌍 속성과 둘레 계산 메서드를 추가하여라. 이 때, Pair 클래스는 연습문제 8-9에서 정의한 것을 활용하도록 한다.

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

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

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

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

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

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

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