15  객체 지향형 파이썬(OOP)

파이썬은 객체 지향형 언어의 일종이다.

15.1 클래스

  • 파이썬 클래스(class)는 (함수처럼) 호출 가능하고, 클래스를 호출하여 인스턴스(instance) 객체를 만든다. 이 과정을 instantiation이라고 한다. 인스턴스의 타입(type)은 클래스이다.
  • 클래스는 임의의 attributes를 가진다.
  • 클래스 attributes의 값으로 정의된 함수를 메서드(methods)라고 한다.
  • 모든 인스턴스 객체는 attribute lookup을 할 때 먼저 인스턴스 안에서 검색을 하고, 만약 없으면서 자신의 class로 옮겨 검색을 한다. 만약 이 class에서도 발견되지 않으면 부모 class로 이동하여 검색한다.
  • 파이썬에서 클래스는 하나의 객체(value)이다. 따라서 다른 객체와 똑같이 취급된다(first-class objects).
    • 함수를 호출할 때 argument로 클래스를 전달할 수 있다.
    • 함수 호출의 결과로 클래스를 반환할 수 있다.
    • 변수에 할당할 수 있다.
    • 컨테이너의 하나의 아이템으로 들어갈 수 있고, 객체의 한 attributes로 지정될 수 있다.
    • 딕셔너리에서 키로 사용할 수 있다.

15.2 클래스 정의하는 방법

클래스 정의는 다음과 같은 문법 구조를 가진다.

class Classname(base-classes, *, **kw):
    statement(s)
  • class로 시작한다.
  • 클래스 이름은 대문자로 시작하는 것이 관례이다(title-case).
  • base-classes는 코마로 구분한다. base-classes를 지정하지 않는다는 것은 object를 사용한다는 의미이고, 이런 경우에는 괄호를 생략할 수 있다.
  • statesment(s) 부분을 클래스 바디(body)라고 한다.

다음은 C1이라는 클래스에서 x라는 attribute를 명시한 예이다. 보는 바와 같이 class body 안에서 이름에 값을 할당하면, 이 이름은 클래스의 attribute로 지정된다.

class C1:
    x = 23

위 코드가 실행되면, C1 클래스가 x라는 attributes를 가지게 되고, 그 값은 23이 된다. 이 attribute의 값에 접근할 때는 dot(.)를 사용하여 C1.x라는 문법을 사용한다.

C1.x
23

위 코드가 실행되면 C1이라는 클래스가 정의되지만 그 인스턴스는 생성된 것은 아니다. 인스턴스는 클래스를 함수 호출과 같은 방식으로 클래스를 호출하여 생성된다.

c1 = C1()

이제 c1라는 C1 클래스의 인스턴스가 만들어졌다. c1type()을 물어보자. C1이라고 답할 것이다.

type(c1)
__main__.C1

어떤 클래스의 모든 인스턴스는 클래스 attributes를 공유한다. 다음은 c2라는 인스턴스를 하나 더 생성했다. c1이나 c2 모두에서 같은 클래스의 attribute x에 접근할 수 있다.

c2 = C1()
c1.x, c2.x
(23, 23)

class 문이 실행될 때, 암묵적으로 해당 클래스에 다음과 같은 attributes가 자동으로 만들어진다.

  • __name__: 클래스의 이름
  • __bases__: 부모 클래스들
  • __dict__: 클래스 attributes를 저장(read-only)
print(C1.__name__)
print(C1.__bases__)
print(C1.__dict__)
C1
(<class 'object'>,)
{'__module__': '__main__', 'x': 23, '__dict__': <attribute '__dict__' of 'C1' objects>, '__weakref__': <attribute '__weakref__' of 'C1' objects>, '__doc__': None}

15.3 메서드(method) 정의

클래스 body 안에서 def 문을 사용하여 만든다. 일반적인 함수 정의 방법과 규칙을 모두 따른다. 단, 첫 번째 파라미터는 인스턴스를 의미하는 self가 되어야 한다. 다른 단어로 써도 되지만 관례상 self로 사용한다. 메서드도 클래스 attributes 일종이므로 클래스의 모든 인스턴스에서 공유되고, . 문법을 사용하여 접근할 수 있고, 함수이기 때문에 ()를 사용하여 호출한다.

class C2:
    x = 23
    def hello(self):
        print(f"내가 가진 x 값은 {self.x}이다.")

my_c = C2()
my_c.hello()
내가 가진 x 값은 23이다.

위에서 보듯이 메서드 안에서 클래스의 attribute를 접근할 때 self.x를 사용했다. class는 namespace를 만들기는 하지만 함수처럼 scoping을 제공하지는 않는다. 그래서 온전한 이름을 써야 한다(fully qualified name). 메서드 안에서 클래스의 다른 메서드를 호출하는 경우에도 똑같이 이렇게 사용한다. self.x 대신 C2.x라고도 할 수 있다. 메서드 밖에서는 아래 y와 같이 이름 그대로 쓸 수 있다.

class C2:
    x = 23
    y = x + 4
    def hello(self):
        print(f"내가 가진 x 값은 {self.x}이다.")

15.4 __init__() 스페셜 메서드

파이썬에는 더블 언더스코어(__)로 되어 있는 미리 이름이 지정되어 특수한 목적으로 사용되는 메서드들이 있다. 이런 메서드를 스페셜 메서드(special method)라고 하고, 던더(dunder) 메서드라고 하기도 하고, 매직 메서드라고도 한다. 그 가운데 __init__() 메서드는 클래스 정의할 때 가장 많이 사용되는 메서드이 때문에 먼저 알 필요가 있다.

__init__() 메서드는 인스턴스의 초기값을 설정하는 데 사용된다.

다음은 Account라는 은행 계좌를 클래스로 구현한 예인데, 이 사례를 가지고 알아 보자.

class Account:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance

    def __repr__(self):
        return f'Account({self.owner!r}, {self.balance!r})'

    def deposit(self, amount):
        self.balance += amount

    def withdraw(self, amount):
        self.balance -= amount

    def inquiry(self):
        return self.balance

이렇게 정의된 Account 클래스의 인스턴스를 만들 때는 다음과 같이 실행한다.

mine = Account("ksb", 10000)
mine
Account('ksb', 10000)

이것을 위 __init__() 메서드 정의와 연관지어 볼 수 있어야 한다.

  • __init__() 첫 번째 파라미터는 다른 메서드와 동일하게 인스턴스를 의미하는 self이다.
  • 하지만 인스턴스를 만들 때는 self를 뺀 나머지 파라미터를 맞추어 Account("ksb", 10000) 클래스 호출을 실행한다. 이 경우 "ksb"owner로, 10000balance로 넘겨진다.
  • 이렇게 넘겨진 값들은 __init__() 메서드 안의 코드에서 따라 self.owner, self.balance로 할당된다.
  • __init__() 함수의 역할을 딱 여기까지이다. 이 함수는 None 값 이외에는 반환값이 없어야 한다. return을 사용하면 에러(TypeError)가 발생한다. 그래서 return 문을 사용하지 않는 것이 관례이다.
a = Account("Guido", 10000)
# 이것은 Account(a, 'Guido', 10000)와 같다.

15.5 __repr__() 스페셜 메서드

위 코드에서 다음과 같이 __repr__() 스페셜 메서드가 정의되어 있다.

def __repr__(self):
    return f'Account({self.owner!r}, {self.balance!r})'

이것은 생성된 인스턴스를 어떻게 문자열로 표시하게 만들지 정의하고자 할 때 사용하는 메서드이다. 사용자가 정의하는 클래스는 일종의 데이터 타입인데, 내장된 리스트나 딕셔너리처럼 문자열로 표시하여 보여주는 것이 유용할 때가 많다. 우리가 만드는 객체의 문자열 표현을 정의하는 것이 __repr__() 메서드의 역할이다.

위 경우 mine 객체는 아래와 같이 표시된다.

mine
Account('ksb', 10000)

이것은 repr()이라는 내장 함수의 작동 방식을 결정한다.

repr(mine)
"Account('ksb', 10000)"

출력된 것을 잘 관찰해 보면 인스턴스를 만들 때의 코드와 같음을 알 수 있다. 보통 이 출력을 가지고 같은 객체를 만들 수 있게 __repr__()을 정의한다.

eval(repr(mine))
Account('ksb', 10000)