데이터는 그 유형이 무엇인가에 따라 취급하는 방법도 다르다. 예를 들어, 수 유형의 데이터가 사칙연산과 반올림 연산 등을 적용할 수 있는 반면, 문자열 유형의 데이터는 길이를 세는 연산이나 특정 문자를 찾는 메서드를 적용할 수 있다. 수를 더하면 합계가 계산되고(1 + 1 == 2)이고, 문자열을 더하면 연결되는('1' + '1' == '11') 것처럼, 데이터 유형이 무엇이냐에 따라 동일한 연산 명령이라도 실제 수행되는 연산이 다를 수 있다.

연산을 데이터의 유형과 결부시키는 것은 데이터 유형을 관리하는 주요 이유 가운데 하나다. 이를 통해 프로그래머가 데이터에 맞지 않는 엉뚱한 연산을 적용하는 실수를 방지할 수 있으며, 동일한 연산자나 함수 이름으로도 문맥(연산을 적용할 데이터들의 유형)에 맞는 다양한 작업이 이루어지도록 할 수 있다. 파이썬에서 데이터 유형은 클래스이며, 각 데이터 유형을 위한 연산은 메서드다. 이 절에서는 클래스에서 일반 함수 대신 메서드를 통해 연산을 제공해야하는 이유와 방법을 알아본다.

8.4.1 인터페이스

현대 사회는 분업 사회다. 그 누구도 자신에게 필요한 모든 것을 스스로 만들어내지 못한다. 문제를 컴퓨터 프로그래밍이라는 비교적 좁은 분야로 한정하더라도 마찬가지다. 혼자서 작은 프로그램 하나를 만들더라도, 누군가가 만들어 둔 개발 도구에 의존하며, 누군가가 만들어 둔 시스템(하드웨어와 운영 체제)을 전제로 하고, 프로그램 속에는 누군가가 수집하고 정리한 수많은 데이터와 누군가가 만들어 둔 수많은 프로그램 조각이 포함된다. 자신이 생산하는 프로그램의 동작 구조와 원리를 그려볼 수 있어야겠지만, 모든 모듈을 직접 개발하려는 노력은 (학습에는 도움이 되지만 산업 프로그래밍에서는) 무모하며 시간낭비일 뿐이다. 석기 사회에서 프로그래밍을 할 수는 없는 법이다.

분업이 잘 이루어지려면 분업 과정에서 전달되는 제품(중간재)을 소비자(중간생산자)가 잘 사용할 수 있어야 한다. 제품에 대해 가장 잘 아는 사람은 생산자이므로 사용법을 소비자에게 전달하는 것도 그의 몫이다. 이를 위해 생산자가 취할 수 있는 방법은 크게 두 가지다. 하나는 정확하고 상세하고 친절한 문서(사용설명서)를 작성하여 제공하는 것이고, 다른 하나는 소비자가 아무리 부주의하더라도 실수하지 않게끔 제품을 쉽고 직관적으로 사용할 수 있게 설계하는 것이다. 전자는 생산자의 기본 의무이고, 후자는 (사용자들이 문서를 읽지 않는 경향이 크다는 이유 때문에) 최근에 점점 더 강조되는 방식이다.

TV를 새로 구입해서 시청할 때를 생각해보자. TV는 분명 내가 만들지 않았고, TV에 공급할 전기도 내가 생산하지 않는다. 하지만 설명서를 읽지 않더라도 별 어려움 없이 TV의 전원 플러그를 콘센트에 꽂고 시청할 수 있다. TV 회사가 TV를 쉽고 안전하게 사용할 수 있도록 만들어 두었고, 전력회사도 전기를 안전하게 사용할 수 있도록 콘센트와 플러그를 표준화하여 제공하는 덕분이다.

프로그램을 만들 때는 남이 만든 모듈(라이브러리)을 활용할 때가 많다. 잘 만들어진 모듈에는 정확하고 자세한 문서가 동봉된다. 모듈을 잘못된 방식으로 사용할 경우 이를 통해 생산된 프로그램도 제대로 동작하지 못할 것이다. 자신이 사용하려는 도구의 문서를 꼼꼼이 읽고, 필요하다면 소스 코드까지 분석하여 사용법을 숙지하는 것이 프로그래머의 의무다. 하지만 반대로 모듈을 만들어 제공하는 프로그래머의 입장에서는, 모듈을 사용하는 프로그래머가 문서를 성실하게 읽을 것이라고 전제해서는 안 된다. 오히려 모듈의 쉽고 직관적으로 사용할 수 있도록 설계하여 사용자가 세부사항을 정확히 알지 못하더라도 함정에 빠지지 않도록 배려해야 한다.

문서가 기본인 것과 더불어 현실적으로는 쉽고 직관적으로 사용할 수 있는 성질(사용성)이 더욱 중요한 셈이다. 사용성은 주로 제품과 사용자가 맞닿는 지점에서 드러나는데, 이 접촉점을 인터페이스(interface)라고 한다. TV를 쓰기 쉬우려면 좋은 리모컨 인터페이스가 필요하고, 전기를 안전하게 사용하려면 콘센트와 플러그의 인터페이스가 안전해야 한다. 간결한 조작으로 필요한 기능을 잘 수행할 수 있는 앱은 사용자 인터페이스가 좋다는 평을 받는다.

프로그램 모듈에서는 모듈의 동작에 필요한 변수, 함수, 클래스 등의 구성이 인터페이스에 해당된다. 오늘날에는 그중에서도 주로 클래스를 활용해 인터페이스를 설계할 때가 많다. 클래스를 통해 어떤 데이터에 어떤 연산을 적용해야하는지를 명확히 정할 수 있기 때문이다. 클래스와 메서드를 활용함으로써, 모듈 제공자는 사용자가 데이터에 엉뚱한 연산을 적용하지 않도록 금지할 수 있고, 사용자는 모듈을 전면 분석하지 않고도 제공자의 의도에 맞게 안전하게 사용할 수 있다.

8.4.2 캡슐화

사용자가 제품을 올바른 방법으로 사용하도록 하는 방법 중 하나는 잘못된 방법으로 사용하지 못하게 막는 것이다. 게임 회사 닌텐도는 게임 카트리지에 고약한 쓴 맛이 나는 화합물을 입혀두어, 아이들이 입에 물고 놀다가 삼키지 않도록 방지했다. 컴퓨터에 다양한 장치를 연결하기 위한 포트들은 다양한 형태의 규격으로 설계되어, 서로 맞지 않는 엉뚱한 전선을 꽂지 못하도록 방지하고 있다.

클래스를 설계할 때도 이 방식을 적용할 수 있다. 클래스에 적용할 수 있는 연산(메서드)만을 제공하고, 클래스의 내부 데이터(속성)는 클래스 외부에서 직접 접근할 수 없도록 차단하는 것이다. 이 경우 설계자가 제공한 메서드를 통해서만 객체를 조작하게 되므로, 클래스의 내부 사정을 잘 모르는 사용자가 객체를 잘못 조작하는 일을 방지할 수 있다. 이렇게 클래스의 속성을 감춘 채 꼭 필요한 메서드만을 외부로 노출하는 클래스 설계 방법을 캡슐화(encapsulation)라고 부른다.

메서드를 이용해 속성 조작하기

연락처를 나타내는 클래스를 정의해 보고, 그 문제점을 살펴보면서 파이썬에서 캡슐화를 수행하는 방법을 알아보려 한다. 아래의 Contact 클래스 정의를 대화식 셸에 입력해 보아라. 코드가 너무 길어 불편하다면 파이썬 프로그램 파일로 작성해도 무방하다.

코드 8-50 연락처를 나타내는 클래스

class Contact:
    """연락처를 나타내는 클래스"""
    def is_valid_number(number):
        """number가 올바른 전화번호 형식인지 확인한다."""
        # 번호가 전부 숫자이고 11자리 길이인가?
        return number.isnumeric() and len(number) == 11
    
    def __init__(self, name, number):
        """인스턴스를 초기화한다."""
        self.name = name         # 이름
        self.numbers = []        # 전화번호 리스트
        self.add_number(number)  # 첫 전화번호를 추가한다
    
    def add_number(self, number):
        """이 연락처에 전화번호를 추가한다."""
        if Contact.is_valid_number(number):  # 전화번호가 올바를 때만
            self.numbers.append(number)      # 전화번호를 추가한다
        else:
            print(number, '는 올바른 전화번호가 아닙니다.')
    
    def remove_number(self, number):
        """이 연락처에서 전화번호를 찾아 제거한다."""
        self.numbers.remove(number)
    
    def print(self):
        """이 연락처를 화면에 출력한다."""
        print('이름:', self.name)
        print('전화번호:')
        for number in self.numbers:
            print('  *', number)

코드를 작성한 뒤에는 클래스를 분석해보고, 이 클래스가 어떤 방식으로 사용해야 하는 것인지 생각해보기 바란다. 남이 만든 코드를 읽고 의도를 파악하는 것은 프로그래밍 실력 향상에 도움이 된다. 다음은 내가 정리한 이 클래스의 특징과 사용법이다. 생각한 것과 어떤 점이 같고 다른지 비교해 보자.

  • 각 연락처 인스턴스는 이름 하나(name)와 전화번호 여러 개(numbers)를 속성으로 가진다.
  • 인스턴스를 생성할 때 초기값으로 이름과 전화번호 하나를 입력한다.
  • 전화번호를 추가할 때는 add_number() 메서드, 삭제할 때는 remove_number() 메서드를 사용한다.
  • Contact.is_valid_number() 메서드를 이용해 전화번호가 올바른지 검사한다. 참고로 이 메서드는 인스턴스용 메서드가 아닌, 클래스 메서드다.
  • 연락처 내용을 화면에 출력할 때는 print() 메서드를 사용한다.

다음은 Contact클래스의 인스턴스를 만들어 사용해 본 예다.

코드 8-51 메서드를 이용해 인스턴스 조작하기

>>> contact_1 = Contact('김파이', '01234567890')
>>> contact_1.add_number('01019172017')     # 연락처 추가
>>> contact_1.add_number('01011112222')     # 연락처 추가
>>> contact_1.remove_number('01011112222')  # 연락처 삭제
>>> contact_1.add_number('0101234????')     # 연락처 추가 (잘못된 형식)
0101234???? 는 올바른 전화번호가 아닙니다.

>>> contact_1.print()  # 연락처 출력
이름: 김파이
전화번호:
  * 01234567890
  * 01019172017

위 코드는 contact_1.numbers 속성을 직접 조작하지 않고 메서드를 통해 사용하는 방법을 보여준다. add_number() 메서드와 remove_number() 메서드를 이용하면 인스턴스 내부 속성이 어떤 식으로 구현되어 있는지 전혀 신경쓰지 않고도 인스턴스의 전화번호를 추가/제거할 수 있다. 이 클래스는 여기에 더해 Contact.is_valid_numbers() 메서드를 통해 입력된 전화번호가 올바른지를 자동으로 검사해주기도 한다. 메서드를 제공하지 않고 클래스 사용자(프로그래머)가 numbers 속성을 직접 조작하도록 했다면 그가 실수로 검사 과정을 빠트릴지도 모를 일이다. 하지만 메서드를 이용하도록 하면 내부 속성을 조작하기 위한 절차를 클래스를 작성하는 프로그래머가 확립하므로 그럴 우려가 없다.

요컨대, 클래스를 설계할 때는 클래스의 내부 속성과 구현 방식을 클래스 안으로 감추고, 클래스를 조작하는 방법만을 메서드로 제공하면 된다. 덕분에 클래스 안에서 어떤 일이 일어나는지, 메서드가 인스턴스를 어떤 절차에 따라 조작하는지는 클래스를 사용하는 사람은 알 필요가 없게 된다. 그럼에도 클래스 설계자는 클래스를 안전하게 제공할 수 있고, 클래스 사용자는 설계자의 의도에 따라 클래스를 사용할 수 있다.

비공개 속성

그런데 파이썬에서는 기본적으로 클래스와 인스턴스의 모든 속성을 외부에서 가리킬 수 있다. 그래서 클래스 사용자는 메서드를 통하지 않고 속성을 직접 사용할 수도 있다. 이를 허용하면 메서드를 통한 인터페이스가 유명무실하게 되는 문제가 있다. 다음 예는 메서드를 이용하지 않고 인스턴스의 속성에 직접 접근했을 때 발생할 수 있는 문제를 나타낸 것이다.

코드 8-52 인스턴스의 속성에 직접 접근하기

>>> contact_1.numbers   # 인스턴스의 속성에 직접 접근할 수 있다
['01234567890', '01019172017']

>>> contact_1.numbers.append('0101234????')  # 속성을 직접 수정
>>> contact_1.numbers   # 잘못된 형식의 번호가 입력되었다
['01234567890', '01019172017', '0101234????']

프로그래머가 클래스의 내부 구성을 알고 있다면 속성을 직접 사용할 수 있을 것이다. 하지만 클래스의 동작 원리를 완전히 이해하지 못하고 있다면 문제를 일으킬 수 있다. add_number() 메서드에서는 입력된 번호를 검사하여 번호가 올바른 형식일 때만 추가하지만, 위 코드는 이 검사를 수행하지 않은 채 속성을 직접 수정하여 잘못된 번호를 추가하는 문제를 일으켰다.

클래스 설계자는 클래스 사용자자 속성을 잘못 조작하는 것을 막기 위해 속성을 비공개로 설정할 필요가 있다. 비공개 속성이란 클래스 내부의 메서드에서는 가리킬 수 있지만 클래스 밖에서는 직접 가리킬 수 없는 속성이다.

C++, 자바 등 비공개 속성을 지원하는 언어도 있으나, 파이썬은 비공개 속성을 지원하지 않는다. 파이썬 문화에서는 속성을 강제로 숨기기보다는 프로그래머가 사람이 양식 있게 행동할 것을 요구한다. 문서에 규정된 사항에 따라 메서드를 통해 클래스를 조작하고, 속성을 직접 사용할 때는 충분한 주의를 기울여야하는 것이다. 단, “이건 비공개 속성이라고 생각하고 직접 접근하지 말아주세요”라는 의미를 나타내는 작명 관례가 있다. _secret처럼 비공개로 취급해야 할 속성의 이름을 밑줄 기호(_) 하나로 시작하는 것이다. 다음은 Contact 클래스에서 비공개로 취급되어야 할 속성의 이름을 밑줄로 시작하도록 수정한 것이다. (지면 절약을 위해 일부만 실었다.)

코드 8-53 관례에 따라 밑줄 기호로 비공개 속성 나타내기

class Contact:
    def _is_valid_number(number):    # 비공개 메서드  _is_valid_number()
        return number.isnumeric() and len(number) == 11
    
    def __init__(self, name, number):
        self._name = name            # 비공개 속성  _name
        self._numbers = []           # 비공개 속성  _numbers
        self.add_number(number)
    
    def add_number(self, number):
        if Contact._is_valid_number(number):
            self._numbers.append(number)

인스턴스의 속성 _name_numbers를 밑줄 기호를 이용해 비공개임을 표시했다. 이제 이 클래스를 사용하는 프로그래머들은 이 속성에 직접 접근해서는 안 되고 공개 메서드를 이용해야 한다는 걸 알아볼 것이다. 참고로 클래스 공용 속성인 _is_valid_number() 메서드도 클래스 내부에서만 사용할 것이므로 비공개로 설정해 두었다.

파이썬은 공식적으로는 비공개 속성을 지원하지 않는다. 밑줄 기호를 이용한 비공개 속성 표시는 어디까지나 프로그래머들 사이의 관례일 뿐이다. 필요하다면 관례를 깨고 밑줄 기호로 시작하는 속성을 조작할 수도 있다. 하지만 양식 있게 행동하는 편이 좋다.

재정의 실수 방지

속성의 이름이 밑줄 기호가 하나가 아니라 두 개(__)로 시작하도록 정의하는 경우도 있다. 밑줄 기호 두 개로 시작하는 속성은 이름 앞에 밑줄 기호와 클래스 이름이 자동으로 붙여져 정의된다. 이것은 어떤 속성이 자식 클래스에 의해 실수로 재정의되는 것을 방지하기 위한 기능(관례가 아니라 공식적인 정의)이다. 다음 예를 보자.

>>> class A:
...     __data = 10  # 밑줄 두 개로 시작하는 속성
... 
>>> A._A__data  # 속성 이름 앞에 _A을 붙여야 가리킬 수 있다.
10

>>> class B(A):  # A를 상속하는 B
...     __data = 20  # 실수로 __data를 재정의
... 
>>> B._A__data   # 상속받은 속성
10
>>> B._B__data   # 재정의한 속성
20

실제 프로젝트에서 이 기능을 사용할 일은 거의 없다. 다만 비공개 속성을 정의할 때 실수로 밑줄 두 개로 시작하지 않도록 주의하자.

연습문제

연습문제 8-12 주사위

다음 조건에 맞춰 주사위를 나타내는 클래스(Dice)를 정의하여라.

  • 각 주사위 객체마다 면의 수가 다르다. 예를 들어 육면체 주사위는 6개의 면을 갖는다. 이 ‘면의 수’를 비공개 인스턴스 속성으로 정의하라.
  • 각 주사위 객체는 항상 어느 한 면이 위를 향하고 있으며, 그 면은 1과 ‘면의 수’ 사이의 자연수다. 이 ‘나온 면’을 비공개 인스턴스 속성으로 정의하라.
  • 주사위 인스턴스는 Dice(sides)과 같이 하나의 인자를 전달하여 생성한다. 인스턴스화 과정에서 ‘면의 수’는 sides가 전달받는 값으로, ‘나온 면’은 자신이 가질 수 있는 임의의 값으로 초기화된다.
  • 인스턴스의 현재 ‘나온 면’을 반환하는 top() 메서드를 정의하라.
  • 인스턴스의 ‘나온 면’을 새 임의의 값으로 설정하고 반환하는 role() 메서드를 정의하라.
  • n 이상 m 이하의 임의의 수는 random.randint(n, m) 함수를 사용하여 구한다. 예를 들어 1 이상 8 이하의 임의의 수는 random.randint(1, 8)이다. (이 함수를 사용하려면 먼저 random 모듈을 임포트해야 한다.)

클래스를 정의한 후 프로그램 하단에 다음 코드를 삽입해 테스트해 보아라.

dice_4 = Dice(4)      # 사면체 주사위 생성
print('사면체 주사위 테스트 ----')
print('처음 나온 면:', dice_4.top())
print('다시 굴리기:', dice_4.roll())
print('다시 굴리기:', dice_4.roll())

dice_100 = Dice(100)  # 백면체 주사위 생성
print('백면체 주사위 테스트 ----')
print('처음 나온 면:', dice_100.top())
print('다시 굴리기:', dice_100.roll())
print('다시 굴리기:', dice_100.roll())

이 프로그램을 실행한 결과는 다음과 같다. 임의의 값이 사용되므로 나온 수는 차이가 있을 것이다.

사면체 주사위 테스트 ----
나온 면: 2
다시 굴리기: 2
다시 굴리기: 1
백면체 주사위 테스트 ----
나온 면: 42
다시 굴리기: 54
다시 굴리기: 79

8.4.3 연산자의 동작 정의하기

특정 클래스를 위한 연산은 주로 메서드를 통해 제공된다. 그런데 클래스 중에는 연산자에 의한 연산을 지원하는 것도 많다. 예를 들어 문자열은 대소문자 변환과 문자 세기 등 다양한 메서드로 연산을 지원하는 한편 문자열과 문자열을 연결하는 연산은 덧셈 연산자(+)를 통해 지원한다. 연산자를 이용하면 특정한 연산을 좀 더 직관적이고 간결하게 나타낼 수 있다. 특히 파이썬의 기본 데이터는 연산자를 통한 연산을 많이 지원한다.

클래스를 이용해 데이터 유형을 직접 정의할 수 있으니, 그 클래스가 연산자를 통해 수행할 연산도 직접 정의할 수 있으면 좋을 것이다. 이항식 ax + by 를 나타내는 클래스를 정의하면서 연산자의 동작을 정의하는 방법을 알아보자.

코드 8-54 이항식을 나타내는 클래스 Binomial

class Binomial:
    """이항식 ax + by 를 나타내는 클래스"""
    def __init__(self, a, b):
        """인스턴스를 초기화한다."""
        self._a = a   # x항의 계수 a
        self._b = b   # y항의 계수 b
    
    def add(self, other):
        """두 이항식의 계수를 더한 새 이항식을 반환한다."""
        a = self._a + other._a  # 두 계수의 합을 구한 후,
        b = self._b + other._b  # 
        return Binomial(a, b)   # 새 이항식을 만들어 반환
    
    def to_string(self):
        """이 이항식을 문자열로 나타낸다."""
        if self._b < 0:
            return str(self._a) + 'x' + str(self._b) + 'y'
        else:
            return str(self._a) + 'x+' + str(self._b) + 'y'

이 클래스는 인스턴스 속성 ab를 이용해 각각 x항의 계수와 y항의 계수를 나타낸다. 그리고 add() 메서드를 이용해 두 항을 서로 더할 수 있으며, to_string() 메서드로 문자열로 표현할 수도 있다. 다음은 그 인스턴스를 사용해 본 예다.

코드 8-55 이항식을 나타내는 클래스 Binomial

>>> exp1 = Binomial(8, 5)
>>> print(exp1.to_string())  # 문자열 표현을 화면에 출력
8x+5y

>>> exp2 = Binomial(3, -4)
>>> print(exp2.to_string())
3x-4y

>>> exp3 = exp1.add(exp2)    # 두 이항식의 합
>>> print(exp3.to_string())
11x+1y

위 코드에서 이항식 객체를 to_string() 메서드로 문자열로 변환하여 print() 함수로 출력해보고, add() 메서드로 두 이항식을 더해 보기도 했다. 모두 의도대로 잘 수행된다. 그런데 to_string() 메서드를 사용하지 않고 화면에 출력하면 어떤 결과가 생길까? 또, add() 메서드 대신 덧셈 연산자(+)로 두 인스턴스를 더하면 어떻게 될까?

코드 8-56 이항식을 나타내는 클래스 Binomial

>>> print(exp1)  # 클래스를 그냥 출력했을 때
<__main__.Binomial object at 0x7fc527d50f60>

>>> exp1 + exp2  # 클래스를 덧셈 연산자로 더할 때
TypeError: unsupported operand type(s) for +: 'Binomial' and 'Binomial'

Binomial 인스턴스 자체를 print() 함수로 출력한 결과는. “메모리의 0x7fc527d50f60번째 위치에 존재하는 Binomial 형식의 객체”라는 정보가 표시될 뿐이다. 이 정보는 프로그래밍에 별 도움이 되지 않는다. 이보다는 인스턴스의 내용을 묘사하는 to_string() 메서드의 결과를 출력하는 편이 더 유용할 것 같다. 또, 덧셈 연산자로 두 객체를 더했을 때는 아예 BinomialBinomial을 더하는 연산이 지원되지 않는다는 TypeError 오류가 발생한다.

이중 밑줄 메서드

클래스에는 인스턴스를 문자열로 표현해야 하는 상황이나 어떤 연산자에 의해 연산되어야 하는 상황에서 묵시적으로 호출되는 메서드를 정의할 수 있다. 이들의 이름은 미리 정해져있는데, __add__()처럼 밑줄 두 개로 시작하고 밑줄 두 개로 끝난다. 메서드의 이름에 따라 호출되는 상황이 정해져 있다. 예를 들어, __add__() 메서드는 덧셈 연산자에 의해 더해질 때 호출되고, __str__() 메서드는 인스턴스를 문자열로 변환해야 할 때 호출된다. 인스턴스를 초기화할 때 호출되는 __init__() 메서드도 이중 하나다. Binomial 클래스에 이 메서드들을 정의해 보자.

코드 8-57 __add__() 메서드와 __str__() 메서드 정의하기

class Binomial:
    """이항식 ax + by 를 나타내는 클래스"""
    def __init__(self, a, b):
        """인스턴스를 초기화한다."""
        self._a = a   # x항의 계수 a
        self._b = b   # y항의 계수 b
    
    # add() 메서드의 이름을 __add__()로 수정
    def __add__(self, other):
        """두 이항식의 계수를 더한 새 이항식을 반환한다."""
        a = self._a + other._a  # 두 계수의 합을 구한 후,
        b = self._b + other._b  # 
        return Binomial(a, b)   # 새 이항식을 만들어 반환
    
    # to_string() 메서드의 이름을 __str__()로 수정
    def __str__(self):
        """이 이항식을 문자열로 나타낸다."""
        if self._b < 0:
            return str(self._a) + 'x' + str(self._b) + 'y'
        else:
            return str(self._a) + 'x+' + str(self._b) + 'y'

위 코드는 코드 8-54에서 add() 메서드와 to_string() 메서드의 이름을 각각 __add__()__str__()로 수정했을 뿐이다. 이렇게 이중 밑줄 메서드를 정의해 두면 특정한 상황에서 자동으로 호출된다. 이제 다시 한 번 객체를 화면에 출력하거나 덧셈 연산자로 더해 보면, 오류 없이 원하는 결과를 얻을 수 있다.

코드 8-58 이중 밑줄 메서드가 자동으로 호출된다

>>> print(Binomial(8, 5) + Binomial(3, -4))
11x+1y

물론, 연산자가 다양항 만큼이나 덧셈(__add__())과 문자열 표현(__str__()) 외에도 다양한 이중 밑줄 메서드가 있다. 아래의 표에서 그 일부를 소개한다. 입문 수준에 알맞고 자주 사용되는 것만 추렸는데도 개수가 적지 않다. 이걸 다 외울 필요는 없으며, 필요할 때 찾아볼 수 있으면 된다. 분명히 알아두어야 할 점은 파이썬에서 다양한 형식의 데이터가 다양한 연산을 지원하는 데 이중 밑줄 메서드를 활용된다는 점이다.

메서드 호출 시점 기능
__init__(self) 인스턴스 생성과정 인스턴스 초기화
__abs__(self) abs(self) 절대값 계산
__add__(self, other) self + other 덧셈 계산
__sub__(self, other) self - other 뺄셈 계산
__mul__(self, other) self * other 곱셈 계산
__truediv__(self, other) self / other 나눗셈 계산
__pow__(self, other) self ** other 거듭제곱 계산
__floordiv__(self, other) self // other 몫 계산
__mod__(self, other) self % other 나머지 계산
__lt__(self, other) self < other 미만 계산
__gt__(self, other) self > other 초과 계산
__le__(self, other) self <= other 이하 계산
__ge__(self, other) self >= other 이상 계산
__eq__(self, other) self == other 동등 계산
__ne__(self, other) self != other 부등 계산
__repr__(self) repr(self) 객체를 식별할 수 있는 문자열 반환
__str__(self) str(self) 객체에 대응하는 문자열 반환
__int__(self) int(self) 객체에 대응하는 정수 반환
__float__(self) float(self) 객체에 대응하는 실수 반환
__bool__(self) bool(self) 객체에 대응하는 불리언 값 반환

표 8-1 이중 밑줄 메서드

표 8-1을 읽을 때, self는 메서드 호출의 기준이 되는 객체, other+, - 등의 이항 연산자에서 우변에 위치하는 객체를 뜻한다는 점을 참고하자. 이 메서드들을 기능에 따라 초기화, 산술연산, 비교연산, 데이터 유형 변환으로 분류할 수 있다. 파이썬에서 자주 사용되는 연산자, 함수도 그와 같이 분류할 수 있을 것이다.

__repr__()__str__()

__repr__() 메서드와 __str__() 메서드는 둘 다 객체에 해당되는 문자열을 반환하지만, 각각의 용도에 차이가 있다. __repr__()이 반환하는 문자열에는 객체를 다른 객체와 구별하는 정보(고유번호, 세부 내용 등)가 포함된다. 이 정보는 프로그래밍(오류 추적)을 위한 것으로, 예를 들어 프로그램의 실행 기록(로그)에서 어떤 객체가 어떤 동작을 수행했는지를 표기하는 데 사용될 수 있다. 반면, __str__()이 반환하는 문자열은 객체의 의미와 내용을 사람이 읽기 좋은 형태로 표현한 것이다. 간단히 말해, __repr__()은 프로그래머를 위한 문자열을, __str__()은 일반 사용자를 위한 문자열을 반환한다.

일반적으로 __str__() 메서드는 정의하지 않더라도 __repr__() 메서드는 정의하는 경우가 많다. 객체를 사용자가 봐야할 때보다는 프로그래머가 봐야할 때가 더 많기 때문이다. 그리고 객체를 문자열로 변환해야 하는 상황에서 __str__() 메서드가 정의되어 있지 않다면 __repr__() 메서드가 대신 호출된다. 하지만 __str__() 메서드가 __repr__() 메서드 대신 호출되지는 않는다. 이를 통해서도 __repr__() 메서드의 상대적 중요성을 알 수 있다. __repr__() 메서드를 직접 정의하지 않으면 <__main__.Binomial object at 0x7fc527d50f60>와 같이 객체의 클래스 이름과 고유번호를 담은 문자열을 반환하는 함수가 기본으로 제공된다.

그동안 사용해 온 정수 연산, 문자열 연산 등은 실제로는 int 클래스와 str 클래스에 정의된 이중 밑줄 메서드를 호출한 것과 같다. 이 점은 이중 밑줄 메서드를 직접 호출하여 확인할 수 있다.

코드 8-59 연산자 대신 이중 밑줄 메서드로 연산하기

>>> number = 10
>>> number.__eq__(20)   # number == 20
False

>>> number.__mul__(5)   # number * 5
50

>>> number.__lt__(20)   # number < 20
True

>>> number.__float__()  # float(number)
10.0

>>> number.__str__()    # str(number)
'10'

>>> number.__bool__()   # bool(number)
True

==, + 등의 연산자와 abs(), int() 함수 등이 실제로 하는 일이란 객체의 이중 밑줄 메서드를 호출하는 것뿐이다. 덕분에 새로 정의하는 클래스에서도 기본 데이터 유형처럼 다양한 연산자를 지원할 수 있다. 이는 메서드 뿐 아니라 연산자도 데이터를 조작하는 인터페이스로 제공할 수 있다는 의미다. 연산자로 수행되어야 할 연산은 메서드로도 제공할 수 있지만, 연산자를 지원함으로써 좀더 간결하고 직관적으로 클래스를 사용하도록 도울 수 있다.

한편, 특정 메서드가 모든 데이터 유형에서 동일한 용도로 사용되어야 한다는 법은 없다는 점에 유의하자. int의 __add__() 메서드는 덧셈 연산을 수행하지만, str의 __add__() 메서드는 연결 연산을 수행한다. + 연산자를 통해 의미를 직관적으로 전달할 수 있는 기능이라면 꼭 덧셈 연산이 아니더라도 허용할 수 있다. 그러나 어떤 클래스의 __add__() 메서드가 두 인스턴스의 크기를 비교하는 연산을 수행한다면 부적절할 것이다. 연산자는 클래스의 인터페이스의 일부이므로, 클래스를 처음 사용하는 사람도 오해 없이 쉽게 사용할 수 있도록 설계하는 것이 바람직하다.

객체의 생성과 소멸을 담당하는 메서드

표 8-1에서 소개하지 않은 이중 밑줄 메서드 중 객체의 생성을 담당하는 __new__() 메서드와 소멸될 때 호출되는 __del__() 메서드가 있다. 다른 객체지향 프로그래밍 언어에서는 생성자와 소멸자라는 메서드가 이 두 메서드와 대응되는데, 주로 객체가 사용할 시스템 자원을 할당하고 해제하는 중요한 역할을 수행한다. 그런데 파이썬에서는 이 두 메서드를 직접 정의해야 하는 경우는 드물다. 핵심 시스템 자원인 메모리의 할당과 해제를 파이썬이 자동으로 수행해주기 때문이다.

다른 많은 프로그래밍 언어에서 생성자의 정의가 중요한 또다른 이유는 생성자가 객체의 초기화에 관여하기 때문이다. 하지만 파이썬은 생성자를 객체의 생성을 담당하는 __new__() 메서드와 초기화를 담당하는 __init__() 메서드로 구별해 두었다. 파이썬에서도 인스턴스의 초기화는 중요하지만, 일반적으로 __new__() 메서드를 손댈 필요 없이 __init__() 메서드만 잘 정의하면 충분하다.

연습문제

연습문제 8-13 이항식 클래스에 연산 추가하기

코드 8-59의 이항식 클래스 Binomial에 뺄셈 연산과 절대값 연산을 추가해 보아라. 클래스를 정의한 후 다음 코드를 추가해 인스턴스의 연산을 검사해 보아라.

exp1, exp2 = Binomial(8, 5), Binomial(-3, -4)

print('두 이항식:          ', exp1, exp2)
print('두 이항식의 절대값: ', abs(exp1), abs(exp2))
print('두 이항식의 합:     ', exp1 + exp2)
print('두 이항식의 차:     ', exp1 - exp2)

이 프로그램의 올바른 실행 결과는 다음과 같다.

두 이항식:           8x+5y -3x-4y
두 이항식의 절대값:  8x+5y 3x+4y
두 이항식의 합:      5x+1y
두 이항식의 차:      11x+9y

연습문제 8-14 연산자 재정의 장난

정수 데이터의 덧셈 연산이 정수 데이터의 곱셈으로 수행되도록 짓궂은 장난을 쳐 보자. 파이썬 대화식 셸에서 필요한 명령을 입력해보고, 그것이 허용되는지 아닌지를 이유와 함께 설명해 보아라.

힌트: int.__add__() 메서드가 호출되어야 할 때 int.__mul__() 메서드가 호출되도록 해야 한다.