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

8.3.1 class 문

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

class 클래스이름:    # ❶ 첫 행
    """독스트링"""   # ❷ 클래스의 의미와 역할 설명
    본문             # ❸ 클래스 공용 속성 정의

❶ 첫 행에는 class 예약어, 클래스의 이름, 그리고 콜론을 쓴다. 클래스의 이름을 지을 때는 파스칼 표기법을 따르는 것이 관례다. ❷ 클래스에도 함수처럼 독스트링을 입력할 수 있다. ❸ 클래스의 본문에는 클래스에 속하는 속성(attribute)을 정의할 수 있다. 속성에 대해서는 잠시후 알아본다.

이름 표기법

  • 파스칼 표기법(PascalCase): PythonProgramming처럼 단어와 단어를 대문자로 구별하는 방법이다. 파이썬 프로그래밍에서 클래스 이름을 지을 때 사용한다.
  • 뱀 표기법(snake_case): python_programming처럼 단어와 단어를 밑줄 기호로 구별하는 방법이다. 파이썬 프로그래밍에서 변수·함수의 이름을 지을 때 사용한다.
  • 낙타 표기법(camelCase): pythonProgamming처럼 대문자로 단어를 구별하되 첫 단어는 소문자로 쓰는 방법이다. 파이썬 프로그래밍에서는 잘 사용되지 않는다.

대화식 셸에 아래 코드를 따라 입력하여, 카페에서 판매하는 ‘케익’ 유형을 나타내는 클래스를 정의해 보자.

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

>>> class Cake:                       # ❶ 첫 행
...     """케익을 나타내는 클래스"""
...     coat = '생크림'               # ❷ 이 클래스의 공용 속성
... 
>>> Cake                              # ❸ 정의한 클래스 확인하기
<class '__main__.Cake'>

❶ 첫 행에서 파스칼 표기법으로 클래스의 이름을 Cake이라고 지었다. ❷에서는 클래스의 공용 속성으로 coat를 정의했다. ❸ 클래스의 이름을 평가하면 정의한 클래스를 확인할 수 있다. 앞에서 배웠듯이, 클래스도 type 클래스에 속하는 인스턴스다. Cake이라는 이름 앞에 붙은 __main__은 이 클래스가 정의된 모듈(10장)을 의미한다.

여러분이 정의한 클래스도 다른 클래스처럼 인스턴스화를 할 수 있다. Cake 클래스의 인스턴스를 생성하고, type() 함수와 isinstance() 함수로 데이터 유형을 확인해보자.

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

>>> cake_1 = Cake()           # ❶ Cake의 인스턴스 만들기
>>> cake_2 = Cake()           # ❷ 인스턴스를 여러 개 만들 수 있다
>>> type(cake_1)              # ❸ cake_1의 데이터 유형 확인
<class '__main__.Cake'>

>>> isinstance(cake_2, Cake)  # ❹ cake_2가 Cake의 인스턴스인지 확인
True

연습문제

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

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

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

8.3.2 속성과 이름공간

속성이란?

속성은 일반적인 변수와 같은 것(즉, 이름이 붙은 데이터)인데, 클래스와 인스턴스 속에 정의된다는 것이 특징이다. 클래스의 속성은 그 클래스와 인스턴스 전체가 공유하는 특성을 나타낸다. 인스턴스의 속성은 개별 인스턴스의 고유한 특성을 나타낸다.

클래스 공용 속성

클래스의 속성은 클래스 객체 뿐 아니라 그 클래스의 모든 인스턴스가 공유한다. 앞에서 클래스 Cake을 정의할 때 본문에 함께 정의해 두었던 coat 속성을 읽어 보자. 속성은 클래스.속성과 같이 클래스 이름 뒤에 점 기호(.)를 붙여 표현한다.

코드 8-21 클래스와 인스턴스의 속성 읽기

>>> Cake.coat    # Cake 클래스의 coat 속성 읽기
'생크림'

>>> cake_1.coat  # cake_1 객체의 coat 속성 읽기
'생크림'

>>> cake_2.coat  # cake_2 객체의 coat 속성 읽기
'생크림'

케익은 밀가루로 만드는 것이고, cake_1cake_2 역시 예외는 아닌 모양이다. 어쨌든 클래스의 속성을 인스턴스에서도 똑같이 읽을 수 있다는 것을 확인했다.

클래스를 정의한 후에 속성을 수정하는 것도 가능하다. 케익 클래스의 속성을 수정해보자.

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

>>> Cake.coat = '초콜릿'  # 기존 속성 수정하기
>>> Cake.coat             # 수정한 속성 확인하기
'초콜릿'

>>> Cake.price = 4000     # 새 속성 추가하기
>>> Cake.price            # 추가한 속성 확인하기
4000

Cake 클래스의 coat 속성을 수정하고, 가격을 나타내는 속성(price)도 새로 추가해 보았다. 그런데, 클래스의 속성을 수정한 뒤에 인스턴스의 속성을 읽으면 어떻게 될까?

코드 8-23 클래스의 속성을 수정한 뒤 인스턴스의 속성 확인하기

>>> cake_1.coat      # ❶ 이전에 생성한 인스턴스의 속성 확인하기
'초콜릿'
>>> cake_1.price
4000

>>> cake_3 = Cake()  # ❷ 인스턴스를 새로 생성하면?
>>> cake_3.coat
'초콜릿'
>>> cake_3.price
4000

❶과 ❷를 보면, 클래스의 속성을 수정하면 과거에 만든 인스턴스와 미래에 만들 인스턴스가 모두 영향을 받는다는 것을 알 수 있다. 클래스의 속성은 모든 인스턴스가 공유하기 때문에, 클래스의 속성을 수정하면 인스턴스에서도 수정된 데이터를 읽을 수 있다.

일반적으로 클래스의 속성은 class 문에서 확정하고, 나중에는 수정하지 않는다. 클래스의 속성은 모든 인스턴스가 공유하는 데이터여서 마구 바꾸면 혼란스럽다. 만약 자주 변경되어야 하는 속성이 있다면, 그 속성은 클래스가 아니라 인스턴스의 속성이어야 할 가능성이 높다.

인스턴스 전용 속성

인스턴스는 자신만의 속성을 가질 수 있다. 인스턴스의 속성은 클래스나 다른 인스턴스와 공유되지 않는다. 인스턴스에 속성을 추가하려면 인스턴스.속성 = 값과 같이 인스턴스 속성 이름을 쓰고 값을 대입하면 된다.

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

>>> cake_1.topping = '블루베리'  # ❶ 인스턴스에 topping 속성 추가하기
>>> cake_1.topping               # ❷ 인스턴스에 속성이 추가되었다
'블루베리'

>>> Cake.topping                 # ❸ 클래스에는 새로 추가한 속성이 없다
AttributeError: type object 'Cake' has no attribute 'topping'

>>> cake_2.topping               # ❹ 다른 인스턴스에도 새로 추가한 속성이 없다
AttributeError: 'Cake' object has no attribute 'topping'

cake_1 객체에 topping 속성을 추가했다. ❷ 이 속성은 cake_1 객체에서만 포함되어 있고, ❸ Cake 클래스나 ❹ 다른 객체에는 포함되어 있지 않다.

이번에는 클래스에 정의해 놓은 공용 속성을 인스턴스에서 수정해보자. 클래스 전체에 영향을 끼칠까?

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

>>> cake_1.coat                 # ❶ Cake 클래스의 coat 속성을 가리키고 있다
'생크림'

>>> cake_1.coat = '아이스크림'  # ❷ cake_1 객체에 coat 속성을 추가한다

>>> cake_1.coat                 # ❸ cake_1은 이제 자신의 고유한 coat 속성을 갖고 있다
'아이스크림'
   
>>> Cake.coat                   # ❹ Cake 클래스는 자신의 coat 속성을 유지하고 있다
'생크림'

>>> cake_2.coat                 # ❺ 다른 객체는 계속 클래스의 coat 속성을 공유하고 있다
'생크림'

cake_1 객체에 인스턴스 속성을 대입하기 전, coat 속성을 읽으면 Cake 클래스의 공용 속성으로 평가되었다. 하지만 ❷ 인스턴스 속성을 대입하면 cake_1 객체에 자신만의 인스턴스 속성 coat가 추가되며, 기존의 공용 속성은 가려진다. 클래스의 속성을 수정한 게 아니라는 점을 주의하자. ❸ 이제 cake_1 객체는 자신만의 coat 속성을 갖지만, ❹ Cake 클래스와 ❺ 다른 인스턴스는 여전히 클래스 속성을 공유한다.

그림 8-2은 이 이름공간의 범위를 그림으로 나타낸 것이다.

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

개념 정리

  • 클래스 속성: 클래스의 전체 특성을 나타내는 정보. 클래스 속에 정의된 이름이며 클래스에 속하는 인스턴스가 공유한다.
  • 인스턴스 속성: 인스턴스의 고유한 특성을 나타내는 정보. 인스턴스 속에 정의된 이름이며 인스턴스마다 각자 따로 갖는다.

이름공간이란?

같은 이름이라도 문맥에 따라 가리키는 대상이 달라진다. ‘파이썬’이라는 이름은 이 책에서는 프로그래밍 언어를 가리키지만, 동물도감에서는 뱀의 한 종류를 가리킨다. 문맥은 간결한 이름으로도 구체적인 대상을 가리킬 수 있게 도와준다. 덕분에 매번 ‘프로그래밍 언어 파이썬 버전 3’이라고 쓰지 않아도 된다.

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

앞의 예제에서 coat라는 속성을 서로 다른 이름공간에서 정의했다. 이름공간이 없다면 각 속성을 표현하기 위해 Cake_coat, cake_1_coat처럼 매번 다른 이름을 길게 붙여야 한다.

이름을 지을 때는 그 이름이 사용될 문맥에서 어떤 의미를 가질지 생각해야 한다. coat는 케익을 덮는 코팅을 뜻하는 이름이다. 같은 이름이라도 그 문맥이 클래스냐 인스턴스냐에 따라 다음과 같이 다른 의미를 내포할 것이다.

  • Cake 클래스의 이름공간에서는 일반적인 케익의 코팅을 의미한다.
  • cake_1 객체의 이름공간에서는 그 케익만의 고유한 코팅을 의미한다.

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

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

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

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

dir() 함수로 Cake 클래스의 속성을 구해 보았다. 직접 정의한 coat 외에도 밑줄 두 개(__)로 시작하고 끝나는 속성이 많이 보인다. 이 속성들은 자동으로 정의된 것인데, 무엇에 쓰이는 것인지는 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’는 ‘방법’이란는 뜻이다. 즉, 메서드는 데이터 유형을 다루는 방법이 정의된 함수다.

메서드 정의하기

메서드는 속성이므로, 클래스 공용 또는 특정 인스턴스 전용으로 정의할 수 있다. 하지만 대부분의 메서드는 클래스 공용 속성으로 정의한다. 데이터 유형을 다루는 방법은 데이터 유형에 따라 정하기 마련이기 때문이다. ‘절대값’ 메서드를 정수에 대해서 정하지, 2나 -3에 대해서 정하지 않는 것이다. 따라서 메서드는 대부분 class 문 안에서 정의한다. class 문 안에 메서드를 포함하면 class 문은 다음과 같은 형태가 된다.

class 클래스이름:
    """독스트링"""
    클래스 공용 속성
    
    def 메서드():
        """이 클래스를 다루는 함수"""
        메서드 본문
    
    ...(필요한 만큼 메서드를 추가 정의)

class 문 안에서 def 문으로 함수를 정의하면 된다. Cake 클래스에 메서드를 추가해 새로 정의해 보자.

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

class Cake:
    """케익을 나타내는 클래스"""
    coat = '생크림'
    #                                                           ❶ 빈 행
    def describe():                                           # ❷ 메서드 정의하기
        """이 케익에 관한 정보를 화면에 출력한다."""
        print('이 케익은', Cake.coat, '으로 덮여 있다.')      # ❸

❶ def 문 앞에는 빈 행을 하나 삽입하는 것이 관례다. 만약 대화식 셸에서 정의하는 경우에는 class 문의 정의가 끝나버리기 때문에 이를 방지하기 위해 샵 기호(#)로 주석 처리해야 한다. 파이썬 파일에서 클래스를 정의할 때는 이렇게 주석 처리할 필요가 없다.

❷ 클래스 안에서 def 문으로 describe() 메서드를 정의했다. coat 속성을 읽어 케익(클래스 또는 인스턴스)이 어떤 재질로 만들어졌는지를 화면에 출력하는 함수다. 메서드는 별도의 이름공간을 가지기 때문에 본문에서 속성을 가리키지 못한다. coat이라고만 쓰면 이름 오류가 발생하므로, Cake.coat와 같이 속성을 가진 클래스의 이름을 붙여야 한다.

메서드는 다른 속성과 마찬가지로 점 기호를 이용해 가리킬 수 있다. 여기에 괄호를 붙여 메서드를 호출해 보자.

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

Cake.describe()

실행 결과:

이 케익은 생크림 으로 덮여 있다.

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

인스턴스를 위한 메서드

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

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

cake_1 = Cake()  # 클래스를 새로 정의했으니, 인스턴스도 새로 생성해야 한다
cake_1.coat = '초콜릿'

인스턴스는 준비 됐다. cake_1.describe()와 같이 인스턴스를 기준으로 메서드를 호출해 볼 것이다. 어떤 실행 결과가 나올지 예상할 수 있겠는가? 다음 중 하나일 것이다.

  1. 이 케익은 초콜릿 으로 덮여 있다.가 출력된다.
  2. 이 케익은 생크림 으로 덮여 있다.가 출력된다.
  3. 오류가 발생한다.

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

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

cake_1.describe()

실행 결과:

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

오류 메시지를 해석해보면, “describe()는 인자를 0개 전달받는 함수인데, 여기에 인자 1개가 전달되었다.”라는 의미다. 이상하다. cake_1.describe() 코드는 인자를 하나도 전달하지 않는데, 인자가 전달되었다니?

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

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

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

class Cake:
    """케익을 나타내는 클래스"""
    coat = '생크림'
    
    def describe(self):                                    # ❶
        """이 케익에 관한 정보를 화면에 출력한다."""
        print('이 케익은', self.coat, '으로 덮여 있다.')   # ❷

describe() 메서드에 인스턴스를 전달받기 위한 매개변수 self를 추가했다.가 추가되었다. ❷ 클래스의 속성 Cake.coat 대신 인스턴스의 속성 self.coat를 화면에 출력하도록 변경했다. ‘self’라는 이름은 ‘자신’을 의미하는 영어 단어에서 딴 것이다. 파이썬에서 인스턴스를 전달받는 메서드의 매개변수 이름으로 이 이름을 사용하는 것이 관례다.

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

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

cake_1 = Cake()  # 클래스를 새로 정의했으니, 인스턴스도 새로 생성해야 한다
cake_1.coat = '초콜릿'
cake_1.describe()

실행 결과:

이 케익은 초콜릿 으로 덮여 있다.

메서드가 의도한대로 동작한다. 메서드는 클래스보다는 개별 인스턴스를 조작하기 위한 것일 때가 많다. 그래서 대부분의 메서드는 self 매개변수를 첫 번째 매개변수로 갖는다. 클래스 정의하는 양식을 수정하여 메서드에 self 매개변수를 추가하는 것을 기본으로 하자.

class 클래스이름:
    """독스트링"""
    클래스 공용 속성
    
    def 메서드(self, ...):
        """이 클래스의 인스턴스를 self 매개변수에 전달받아 처리하는 함수"""
        메서드 본문
    
    ...(필요한 만큼 메서드를 추가 정의)

이와 같이 인스턴스를 전달받도록 메서드를 정의해 두면, 클래스를 기준으로 메서드를 호출했을 때 오류가 발생한다. 메서드가 인스턴스를 전달받아야 하는데, 클래스를 기준으로 호출하면 인스턴스가 전달되지 않기 때문이다. 인스턴스를 위한 메서드를 클래스를 기준으로 호출하는 것은 이치에도 맞지 않는다.

코드 8-33 인스턴스를 전달받는 메서드를 클래스 객체에서 호출하면 오류가 발생한다

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

메서드는 여러 인스턴스가 공유하기 때문에 클래스의 속성으로 정의한다. 그런데 메서드는 클래스가 아니라 인스턴스를 기준으로 실행할 때가 많다. 정의하는 곳과 사용하는 곳이 다르다. 헷갈리지 않도록 주의하자.

개념 정리

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

연습문제

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

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

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

>>> point_1.distance(point_2)
3.1622776601683795

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

8.3.4 인스턴스의 초기화

케익에 꽂힌 초의 개수를 표현한다면, 클래스의 속성과 인스턴스의 속성 중 어느 것으로 하는 것이 바람직할까? 모든 케익에 똑같은 개수의 초를 꽂는다면 클래스의 속성이 좋을 것이다. 하지만 실제로는 케익을 누구에게 선물하는지에 따라 꽂는 초의 개수가 다를 것이다. 이처럼 각 인스턴스마다 다른 정보를 가져야 한다면 그 정보는 인스턴스의 속성으로 정의해야 한다.

인스턴스의 속성은 객체를 만들자마다 정의해두는 것이 좋다. 그래야 모든 인스턴스에 그 속성이 있을 거라고 믿고 이용할 수 있을 것이다. 이처럼 객체를 생성한 직후에 초기 속성을 정의하는 것을 초기화(initialization)라고 한다. 하지만 인스턴스를 만들 때마다 속성을 대입하는 것은 불편하다. 예를 들어, Cake 클래스의 인스턴스를 3개 만들고 각각 초의 개수를 뜻하는 candles속성을 0으로 초기화한다고 해 보자.

코드 8-34 인스턴스의 속성을 일일이 초기화하기가 번거롭다

cake_1 = Cake()
cake_2 = Cake()
cake_3 = Cake()
cake_1.candles = 0
cake_2.candles = 0
cake_3.candles = 0

초기화하는 속성이 candles 하나 뿐인데도 코드가 여러 줄 필요하다. 이런 방식으로는 인스턴스의 개수와 속성의 개수의 곱만큼 작성해야 하는 코드가 늘어나 몹시 불편하다. 좀 더 편리하게 인스턴스를 초기화하는 방법을 알아보자.

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

우리가 Cake()과 같이 인스턴스화를 명령하면, 파이썬은 다음 두 단계에 걸쳐 인스턴스화를 수행한다.

  1. __new__() 메서드를 실행해 새 객체를 만든다.
  2. __init__() 메서드를 실행해 객체를 초기화한다.

__new__ 메서드는 새 객체를 만드는 방법을 담은 메서드인데, 파이썬이 기본으로 제공하기 때문에 여러분이 직접 정의할 필요는 없다. __init__()는 새로 만들어진 객체의 속성을 초기화하는 메서드다. 기본 제공되는 __init__() 메서드는 객체에 아무런 속성도 부여하지 않지만, 여러분이 이 메서드를 직접 정의하면 인스턴스의 초기화 방법을 지시할 수 있다. 다음은 코드 8-31의 Cake 클래스에 __init__() 메서드를 추가한 것이다.

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

class Cake:
    """케익을 나타내는 클래스"""
    coat = '생크림'
    
    def __init__(self, candles):                          # ❶
        """인스턴스를 초기화한다."""
        self.candles = candles
    
    def describe(self):
        """이 케익에 관한 정보를 화면에 출력한다."""
        print('이 케익은', self.coat, '으로 덮여 있다.')
        print('초가', self.candles, '개 꽂혀 있다.')      # ❷

__init`__ 메서드를 새로 추가했다. 매개변수로는 생성된 인스턴스를 전달받을 self와 초기값으로 지정할 값을 전달받을 candles를 정의했다. 전달받은 candles는 메서드 본문에서 self.candles에 대입하여 케익의 속성으로 초기화했다. ❷ describe() 메서드에서 인스턴스 속성 candles를 화면에 출력하도록 했는데, 이제 모든 객체가 초기화 과정에서 candles 속성을 갖게 될 것이므로 문제 없이 실행될 것이다. 이 클래스를 실제로 인스턴스화해 보자.

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

cake_1 = Cake(12)   # 이제 초기값을 지정하여
cake_2 = Cake(100)  # 인스턴스화할 수 있다

print('케익 1:')
print('초 개수:', cake_1.candles)
12

print('케익 2:')
cake_2.describe()
이 케익은 생크림 으로 덮여 있다.
초가 100 개 꽂혀 있다.

실행 결과:

케익 1:
초 개수: 12
케익 2:
이 케익은 생크림 으로 덮여 있다.
초가 100 개 꽂혀 있다.

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

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

__init__() 메서드를 잘 정의해두면 인스턴스화 과정에서 초기화가 함께 수행되도록 할 수 있다. 대부분의 객체는 초기화가 필요하므로, 클래스를 정의할 때 __init__() 메서드를 함께 정의해야 할 때가 많다. 그러므로 클래스 정의 양식을 다음과 같이 확장해 기억하자.

class 클래스이름:
    """독스트링"""
    클래스 공용 속성
    
    def __init__(self, ...):
        """인스턴스를 초기화한다."""
        인스턴스 전용 속성 초기화
    
    def 메서드(self, ...):
        """이 클래스의 인스턴스를 self 매개변수에 전달받아 처리하는 함수"""
        메서드 본문
    
    ...(필요한 만큼 메서드를 추가 정의)

인스턴스가 가져야 할 속성

클래스 공용 속성으로는 메서드와 그 범주의 일반적 속성을 정의하고, 인스턴스 전용 속성으로는 개별 객체의 고유한 속성을 정의해야 한다. __init__() 메서드는 인스턴스가 가져야 할 속성을 정하는 역할을 한다.

각 케익마다 토핑, 가격, 초 개수를 모두 다르게 취급한다고 하자. 그러면 이 속성들은 인스턴스의 속성이 되어야 한다. Cake 클래스의 모든 속성을 인스턴스 속성으로 수정해 보자.

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

class Cake:
    """케익을 나타내는 클래스"""
    coat = '생크림'
    
    def __init__(self, topping, price, candles=0):
        """인스턴스를 초기화한다."""
        self.topping = toping   # 케익에 올린 토핑
        self.price = price      # 케익의 가격
        self.candles = candles  # 케익에 꽂은 초 개수
    
    def describe(self):
        """이 케익에 관한 정보를 화면에 출력한다."""
        print('이 케익은', self.coat, '으로 덮여 있다.')
        print(self.topping, '을 올려 장식했다.')
        print('가격은', self.price, '원이다.')
        print('초가', self.candles, '개 꽂혀 있다.')

Cake 클래스는 개별 케익 객체가 가져야 할 정보를 __init__() 메서드가 전달받아 초기화한다. __init__ 메서드를 보면 인스턴스 속성으로 topping, price, candles가 있다는 것을 알 수 있다. 케익이 처음 나올 때는 초를 꽂아두지 않을 것이므로, cendles 매개변수는 기본값을 0으로 지정해 두었다. 이제 Cake 클래스를 인스턴스화할 때 케익의 속성을 모두 지정할 수 있다.

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

cake_1 = Cake('눈사람 사탕', 10000)
cake_2 = Cake('한라봉', 9000, 8)

print('케익 1:')
cake_1.describe()

print('케익 2:')
cake_2.describe()

실행 결과:

케익 1:
이 케익은 생크림 으로 덮여 있다.
눈사람 사탕 을 올려 장식했다.
가격은 10000 원이다.
초가 0 개 꽂혀 있다.
케익 2:
이 케익은 생크림 으로 덮여 있다.
한라봉 을 올려 장식했다.
가격은 9000 원이다.
초가 8 개 꽂혀 있다.

객체를 생성할 때 __init__ 메서드에 필요한 값을 충분히 제공하지 않으면 오류가 발생한다. 초기화에 필요한 정보가 모자라면 인스턴스의 생성이 이루어지지 않는 셈이다. 인스턴스에 필요한 속성을 모두 __init__ 메서드에 정의해두면, 속성이 누락된 객체가 만들어지지 않게 방지할 수 있다.

코드 8-39 초기화에 필요한 속성이 누락될 경우 인스턴스 생성이 방지된다

cake_2 = Cake('복숭아')

실행 결과:

TypeError: __init__() missing 1 required positional argument: 'price'

개념 정리

  • 객체를 만들 때 객체의 초기 속성을 정해주는 것을 초기화라고 한다.
  • __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