이제 데이터 유형을 클래스로 직접 정의해 볼 차례다. 클래스를 정의한 뒤에는 그 클래스에 속하는 인스턴스로 생성해 볼 것이다. 클래스·인스턴스의 이름공간과 그 속의 속성에 대해서도 알아본다.

8.3.1 class 문

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

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

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

이름 표기 관례

  • 파스칼 표기법: PythonProgramming처럼 단어와 단어를 대문자로 구별하는 방법이다. 파이썬 프로그래밍에서는 클래스 이름을 지을 때 주로 사용한다.
  • 낙타 표기법(camelCase): pythonProgamming처럼 대문자로 단어를 구별하되 첫 단어는 소문자로 쓰는 방법이다. 파이썬 프로그래밍에서는 많이 사용되지 않지만, 자바 코드에서 쉽게 볼 수 있다.
  • 뱀 표기법(snake_case): 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 좌표 클래스 정의하기

좌표를 나타내는 클래스 Coordinate를 정의해 보아라. 이 클래스의 속성으로 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.5절에서 알아볼 것이다.

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

연습문제

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

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

  • 첫번째 인스턴스(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']))

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

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

>>> 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에서 정의한 Coordinate 클래스를 새로 정의하여, 두 좌표 사이의 거리를 계산해 반환하는 메서드 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에서 정의한 Coordinate 클래스를 새로 정의하여, 인스턴스화 과정에서 속성 x와 속성 y를 초기화할 수 있도록 해 보아라. 인스턴스화할 때 초기값을 전달되지 않은 경우에는 기본값 0으로 초기화되도록 해라.

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

>>> point_1 = Coordinate(-1, 2)
>>> point_2 = Coordinate(y=3, x=2)
>>> point_3 = Coordinate()
>>> point_4 = Coordinate(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