← 이전 | 목록 | 다음 →

클래스

08강 마지막에 이런 말을 했었죠. print()는 마법이 아니라 그냥 함수였다고.

오늘은 한 단계 더 나가봅시다. 사실 파이썬에서 여러분이 지금까지 만졌던 거의 모든 것 — 숫자, 문자열, 리스트, 심지어 함수까지 — 이 전부 객체(object)입니다.

그리고 그 객체를 찍어내는 틀을, 클래스(class)라고 부릅니다.

클래스란

붕어빵 틀을 생각해봅시다. 틀 자체는 먹을 수 없습니다. 그 틀에 반죽을 부어서 나온 붕어빵이 진짜 먹는 겁니다.

틀 하나로 붕어빵을 몇 개든 찍어낼 수 있듯이, 클래스 하나로 객체를 몇 개든 만들 수 있습니다.

한번 만들어봅시다.

class Dog:
    pass

class라는 키워드, 뒤에 이름(관례상 대문자로 시작합니다), 그다음 콜론. 또 이 생김새입니다. def, if, for… 파이썬은 진짜 한번 정한 문법을 끝까지 우려먹습니다.

pass는 “여기 아무것도 안 넣을 건데, 그래도 문법상 뭔가는 있어야 해서 넣은 자리 채우기용”입니다. 06강에서 배운 콜론+들여쓰기 규칙, 여기서도 뭔가는 있어야 하거든요.

이 틀로 객체(붕어빵)를 만들어봅시다.

class Dog:
    pass

my_dog = Dog()

Dog()처럼 클래스 이름에 괄호를 붙여서 부르면, Dog라는 틀로 찍어낸 객체 하나가 만들어집니다. 이것도 낯이 익습니다. print(), range()도 이름 뒤에 괄호였습니다. 실은 여러분이 지금까지 부르고 다닌 print(), str() 같은 것도 사실 어딘가의 클래스가 찍어낸 결과물이거나 그 클래스 자체입니다. (너무 깊이 들어가면 오늘 하루가 다 갑니다. 여기까지만 알아둡시다.)

__init__과 self

빈 틀은 재미없습니다. 붕어빵 만들 때 처음부터 팥소를 넣듯이, 객체를 만드는 순간 값을 미리 채워 넣고 싶을 때가 있습니다. 그럴 때 __init__이라는 특별한 메서드를 씁니다.

class Dog:
    def __init__(self, name):
        self.name = name

my_dog = Dog("바둑이")
print(my_dog.name)

결과:

바둑이

하나씩 봅시다.

헷갈릴 만합니다. 양쪽에 똑같이 name이라는 단어가 나오는데, 오른쪽 name은 그냥 매개변수(08강에서 배운 그 매개변수)고, 왼쪽 self.name은 객체한테 딱 붙어서 평생 따라다니는 값입니다. 다른 이름을 써도 되지만(예: def __init__(self, dog_name): self.name = dog_name), 관례상 다들 이렇게 똑같이 씁니다.

붕어빵을 여러 개 만들어봅시다.

class Dog:
    def __init__(self, name):
        self.name = name

dog1 = Dog("바둑이")
dog2 = Dog("초코")

print(dog1.name)
print(dog2.name)

결과:

바둑이
초코

같은 틀에서 나왔지만, dog1이랑 dog2는 서로 다른 name을 각자 따로 기억합니다. 08강에서 클로저 배울 때 double이랑 triple이 각자 다른 n을 기억했던 것과 비슷한 정신입니다. 각자 자기 상태를 갖고 사는 겁니다.

메서드

클래스 안에 함수를 더 넣어서, 그 객체가 할 수 있는 행동을 정의할 수 있습니다.

class Dog:
    def __init__(self, name):
        self.name = name

    def bark(self):
        print(self.name + ": 멍멍!")

my_dog = Dog("바둑이")
my_dog.bark()

결과:

바둑이: 멍멍!

my_dog.bark()라고 부르면, 파이썬이 알아서 my_dog를 self 자리에 넣어서 bark(my_dog)처럼 실행합니다. 여러분은 그냥 점(.) 찍고 메서드 이름만 부르면 됩니다.

던더 메서드와 연산자 오버로딩

아까 __init__이 던더 메서드라고 했었죠. 사실 이런 던더 메서드가 여러 개 더 있고, 이걸 잘 쓰면 여러분이 만든 클래스가 파이썬의 기본 문법(+, ==, print() 등)이랑 자연스럽게 어울리게 만들 수 있습니다.

일단 문제 상황부터 봅시다.

class Dog:
    def __init__(self, name):
        self.name = name

my_dog = Dog("바둑이")
print(my_dog)

결과:

<__main__.Dog object at 0x7f3a2c1b4d90>

이게 뭔 소리냐고요? 파이썬 나름의 성의입니다. “이건 Dog라는 클래스의 객체고, 컴퓨터 메모리 어디쯤(0x7f3a2c1b4d90)에 저장되어 있다”는 걸 알려주는 겁니다. 근데 사람이 보기엔 하나도 안 예쁩니다.

이걸 예쁘게 만드는 던더 메서드가 __str__입니다.

class Dog:
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name + "라는 이름의 개"

my_dog = Dog("바둑이")
print(my_dog)

결과:

바둑이라는 이름의 개

print(my_dog)를 실행하면, 파이썬은 속으로 my_dog.__str__()을 불러서 그 결과를 화면에 찍습니다. 여러분이 __str__을 직접 정의해두면, print()가 여러분이 원하는 모습으로 객체를 보여줍니다.

이제 진짜 재밌는 걸 해봅시다. 05강에서 연산자(+, == 등등)를 배웠죠. 그 연산자들이 숫자나 문자열한테만 특별대우를 받는 게 아닙니다. 여러분이 만든 클래스한테도 +가 뭘 뜻하는지 직접 정의해줄 수 있습니다. 이걸 연산자 오버로딩(operator overloading)이라고 부릅니다.

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return "(" + str(self.x) + ", " + str(self.y) + ")"

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

v1 = Vector(1, 2)
v2 = Vector(3, 4)

result = v1 + v2
print(result)

결과:

(4, 6)

v1 + v2라고 쓰는 순간, 파이썬은 속으로 v1.__add__(v2)를 부릅니다. __add__ 안에서 self는 v1, other는 v2가 됩니다. 그 안에서 x끼리 더하고 y끼리 더한 새 Vector를 만들어 돌려줍니다.

원래 +는 숫자나 문자열만 알아듣는 연산자였는데, Vector 클래스에다 __add__를 정의해주는 순간 Vector끼리도 +가 통하게 됩니다. 파이썬이 원래 봐주던 애들 사이에 여러분이 만든 클래스를 슬쩍 끼워 넣은 겁니다.

자주 쓰는 던더 메서드를 몇 개 더 보여드리겠습니다.

class Vector:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __str__(self):
        return "(" + str(self.x) + ", " + str(self.y) + ")"

    def __add__(self, other):
        return Vector(self.x + other.x, self.y + other.y)

    def __eq__(self, other):
        return self.x == other.x and self.y == other.y

    def __lt__(self, other):
        return (self.x ** 2 + self.y ** 2) < (other.x ** 2 + other.y ** 2)

v1 = Vector(1, 2)
v2 = Vector(1, 2)
v3 = Vector(5, 5)

print(v1 == v2)
print(v1 < v3)

결과:

True
True

정리하면 이런 식입니다.

+   →  __add__
-   →  __sub__
*   →  __mul__
==  →  __eq__
!=  →  __ne__
<   →  __lt__
>   →  __gt__
len()  →  __len__

다 외울 필요 없습니다. 필요할 때 “이 연산자, 던더 메서드 뭐였지” 하고 검색하면 됩니다. 오늘은 “연산자도 사실 뒤에서는 던더 메서드를 부르는 것뿐이었다”는 사실만 챙겨가세요. 05강에서 배운 연산자들이 사실 그 뒤에 함수 호출을 숨기고 있었던 겁니다.

상속

이제 클래스 얘기에서 제일 중요한 부분, 상속(inheritance)을 제대로 다뤄봅시다.

여러 클래스가 비슷한 걸 공유할 때가 있습니다. 강아지도 고양이도 다 “동물”이고, 이름이 있고, 소리를 냅니다. 매번 처음부터 다 쓰는 대신, 공통된 걸 부모 클래스에 몰아넣고 자식 클래스가 물려받게 할 수 있습니다.

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(self.name + "이(가) 소리를 냅니다.")

class Dog(Animal):
    pass

my_dog = Dog("바둑이")
my_dog.speak()

결과:

바둑이이(가) 소리를 냅니다.

class Dog(Animal):처럼 클래스 이름 뒤 괄호에 다른 클래스를 적으면, Dog는 Animal의 모든 걸 물려받습니다. Dog 안에 아무것도 안 썼는데(pass), __init__도 speak()도 다 그냥 작동합니다. Animal이 부모(parent class, super class), Dog가 자식(child class, subclass)입니다.

메서드 오버라이딩

자식이 부모 걸 그대로 쓰기만 하는 게 아니라, 자기 방식대로 다시 정의할 수도 있습니다. 이걸 오버라이딩(overriding)이라고 부릅니다. (방금 배운 연산자 오버로딩이랑 이름이 비슷한데 다른 개념입니다. 헷갈리지 마세요. 이 강의도 자주 헷갈립니다.)

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(self.name + "이(가) 소리를 냅니다.")

class Dog(Animal):
    def speak(self):
        print(self.name + ": 멍멍!")

class Cat(Animal):
    def speak(self):
        print(self.name + ": 야옹!")

animals = [Dog("바둑이"), Cat("나비"), Animal("정체불명")]

for animal in animals:
    animal.speak()

결과:

바둑이: 멍멍!
나비: 야옹!
정체불명이(가) 소리를 냅니다.

Dog랑 Cat은 speak()를 자기 방식대로 새로 정의해서, 부모의 speak()를 덮어썼습니다. Animal 그대로인 애는 부모의 원래 speak()가 실행됩니다.

07강에서 배운 for문으로 서로 다른 종류(Dog, Cat, Animal)를 한 리스트에 넣고 똑같이 .speak()라고 불렀는데, 각자 자기 방식대로 알아서 반응했습니다. 이렇게 “같은 이름으로 불러도 실제 객체 종류에 따라 다르게 동작하는 것”을 다형성(polymorphism)이라고 부릅니다. 있어 보이는 단어인데, 방금 여러분이 이미 한 겁니다.

super()로 부모 것도 같이 쓰기

자식 클래스에서 부모 것을 완전히 버리지 않고, 부모 걸 쓰면서 뭘 더 추가하고 싶을 때가 있습니다. 그럴 땐 super()로 부모에 접근합니다.

class Animal:
    def __init__(self, name):
        self.name = name

class Dog(Animal):
    def __init__(self, name, breed):
        super().__init__(name)
        self.breed = breed

    def info(self):
        print(self.name + " (" + self.breed + ")")

my_dog = Dog("바둑이", "진돗개")
my_dog.info()

결과:

바둑이 (진돗개)

Dog는 Animal한테 없던 breed(품종)를 추가로 받고 싶습니다. 그런데 name을 저장하는 부분은 이미 Animal.__init__에 있으니 그대로 재활용하고 싶습니다. super().__init__(name)이 정확히 그 일을 합니다. “부모의 __init__을 불러서, name 저장하는 일은 거기 맡기고, 나는 그다음에 breed만 추가로 처리한다”는 뜻입니다.

부모 걸 완전히 새로 쓰지 않고 재사용하면서 확장하는 것, 이게 상속을 쓰는 진짜 이유입니다. 08강에서 함수로 코드를 재사용했던 것과 똑같은 정신이, 클래스 단위로 커진 겁니다.

ABC (추상 클래스)

마지막으로 조금 고급 개념 하나만 소개하고 가겠습니다. ABC(Abstract Base Class, 추상 기반 클래스)입니다.

아까 Animal을 상속받은 Dog, Cat이 각자 speak()를 재정의했었죠. 근데 만약 누가 실수로 speak()를 깜빡하고 안 만든 자식 클래스를 만들면 어떻게 될까요? 그냥 부모의 speak()가 그대로 실행됩니다. 파이썬은 아무 불평도 안 합니다.

“자식 클래스는 반드시 speak()를 직접 구현해야 한다”고 강제하고 싶을 때, ABC를 씁니다.

from abc import ABC, abstractmethod

class Animal(ABC):
    def __init__(self, name):
        self.name = name

    @abstractmethod
    def speak(self):
        pass

class Dog(Animal):
    def speak(self):
        print(self.name + ": 멍멍!")

my_dog = Dog("바둑이")
my_dog.speak()

결과:

바둑이: 멍멍!

만약 자식이 speak()를 안 만들면 어떻게 될까요?

from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

class Rock(Animal):
    pass

my_rock = Rock()

결과:

TypeError: Can't instantiate abstract class Rock without an implementation for abstract method 'speak'

Python이 아주 구체적으로 화냅니다. “Rock은 추상 클래스라 객체를 못 만든다, speak라는 추상 메서드를 구현 안 했으니까”라고요. 실수를 미리 막아주는 겁니다. 이게 ABC를 쓰는 이유입니다. “이 부모를 상속받으려면 최소한 이건 꼭 구현해라”라는 계약서를 써두는 겁니다.

여러 사람이 같이 큰 프로그램을 짤 때 이런 계약서가 특히 유용합니다. 지금 당장은 “이런 게 있다” 정도만 알아두셔도 충분합니다.

그 밖에 알아만 두면 좋은 것들

클래스 얘기는 사실 끝이 없습니다. 여기서는 이름만 슬쩍 던지고 지나가겠습니다. 궁금하면 나중에 각자 검색해보세요.

결론

오늘은 클래스를 배웠습니다.

class로 클래스 만드는 법도 배웠고, __init__이랑 self도 배웠고, 메서드도 배웠고, __str__이랑 연산자 오버로딩(__add__, __eq__, __lt__)도 배웠고, 상속, 오버라이딩, super(), 다형성까지 제대로 배웠고, ABC로 추상 클래스 만드는 법도 배웠습니다.

붕어빵 틀 하나 만드는 법을 배우려다가, 어느새 계약서 쓰는 법까지 배웠습니다. 원래 이렇게 됩니다.

마법입니다.

이제 10강으로 넘어가십시오(방금 슬쩍 넘어갔던 import를 제대로 배울 것입니다).

← 이전 | 목록 | 다음 →