정보

클래스와 인스턴스

이름 목록과 점수 목록을 따로 관리하면
언젠가 줄이 어긋납니다.
한 사람에 관한 값은 하나로 묶어야 합니다.

학습 목표

  1. 한 대상에 관한 값들을 따로 관리할 때 생기는 문제를 설명할 수 있다.
  2. 클래스와 인스턴스의 관계를 설명하고, 속성과 메서드를 갖춘 간단한 클래스를 만들 수 있다.
  3. 같은 클래스로 만든 여러 인스턴스가 각자 값을 갖는다는 것을 알고, 클래스를 쓸 때와 쓰지 않아도 될 때를 구별할 수 있다.

따로 든 목록은 언젠가 어긋난다

그리고 어긋나도 오류는 나지 않습니다.

간단한 게임을 만든다고 해 봅시다. 캐릭터마다 이름 · 체력 · 레벨을 저장해야 합니다. 리스트만 쓴다면 목록 세 개를 만들게 됩니다.

names = ['용사', '마법사', '궁수']
hps = [120, 80, 95]
levels = [5, 4, 6]

print(names[1], hps[1], levels[1])

마법사 80 4가 나옵니다. 잘 실행됩니다. 그런데 이 방식에는 깨지기 쉬운 약속이 하나 숨어 있습니다. 세 목록의 같은 인덱스가 모두 같은 캐릭터를 가리켜야 한다는 약속입니다. 코드 어디에도 적혀 있지 않고, 만든 사람이 기억하고 있을 뿐입니다.

마법사가 게임에서 빠진다면

세 목록에서 각각 인덱스 1의 값을 지워야 합니다. 그런데 levels에서 지우는 것을 빠뜨렸다고 해 봅시다. 그러면 그때부터 궁수의 이름에 마법사의 레벨이 붙습니다. 오류는 나지 않습니다. 프로그램은 멀쩡히 실행되고, 숫자만 조용히 틀립니다. 목록이 셋이 아니라 열 개라면 이런 일이 일어날 확률은 훨씬 높아집니다.

문제의 뿌리는 한 캐릭터에 관한 값들이 세 곳에 흩어져 있다는 것입니다. 그렇다면 답도 분명합니다. 한 캐릭터의 값은 하나로 묶어 두면 됩니다. 그러면 캐릭터를 지울 때 그 묶음 하나만 지우면 되고, 어긋날 일이 없습니다.

스스로 확인

학급 명단을 이름 목록 · 번호 목록 · 연락처 목록 셋으로 나누어 관리한다면 어떤 사고가 날 수 있을까요? 한 가지를 들어 보세요.

틀을 하나 만들어 두고 여럿을 찍어 낸다

틀은 하나, 찍어 낸 것은 여럿입니다.

캐릭터를 묶는다고 할 때, 캐릭터마다 어떤 값을 갖는지는 모두 같습니다. 용사도 마법사도 이름과 체력을 갖습니다. 값만 다를 뿐입니다. 그렇다면 「캐릭터란 이름과 체력을 갖는 것」이라는 틀을 한 번만 정해 두고, 그 틀로 캐릭터를 찍어 내면 됩니다.

클래스(class)

어떤 것이 무슨 값을 갖는지 정해 둔 틀입니다. 무슨 일을 할 줄 아는지도 함께 정해 둘 수 있습니다. 틀에는 캐릭터마다 다른 값이 들어 있지 않습니다.

인스턴스(instance)

그 틀로 실제로 만들어 낸 것 하나하나입니다. 각자 자기 값을 갖습니다.

틀 하나에서 여럿이 나온다 Character name hp 용사 · 체력 120 마법사 · 체력 80 궁수 · 체력 95 틀에는 속성의 이름만 있고, 값은 찍어 낸 것마다 다르다
class Character:
    def __init__(self, name, hp):
        self.name = name
        self.hp = hp


hero = Character('용사', 120)
mage = Character('마법사', 80)

print(hero.name, hero.hp)
print(mage.name, mage.hp)

용사 120과 마법사 80이 나옵니다. Character('용사', 120)이라고 호출하면 틀로 하나를 찍어 냅니다. 찍어 낸 것에 저장된 값을 꺼낼 때는 점을 찍어 hero.name처럼 씁니다. 이렇게 인스턴스가 갖는 값을 속성이라고 합니다.


낯선 두 가지 — __init__과 self

코드 안에 처음 보는 것이 둘 있습니다. 하나씩 풀어 보겠습니다.

__init__

새로 하나를 찍어 낼 때 딱 한 번 실행되는 함수로, 생성자(constructor)라고 합니다. 찍어 낸 인스턴스의 속성에 처음 값을 저장하는 일을 맡습니다.

Character('용사', 120)이라고 호출하면 이 함수가 자동으로 호출되어 이름 속성에 「용사」, 체력 속성에 120을 저장합니다.

self

지금 다루고 있는 바로 그 하나를 가리키는 말입니다.

틀에는 이름 속성이 있지만 그 속성이 누구의 것인지는 찍어 낸 뒤에야 정해집니다. self.name은 「지금 이 인스턴스의 이름 속성」이라는 뜻입니다.

self.name = name이라는 줄이 특히 헷갈립니다. 왼쪽과 오른쪽이 서로 다른 것이기 때문입니다. 오른쪽 name은 밖에서 전달받은 매개변수이고, 왼쪽 self.name은 지금 만들고 있는 이 캐릭터의 이름 속성입니다. 전달받은 값을 자기 속성에 대입하는 줄이라고 읽으면 됩니다.

스스로 확인

이 클래스에 레벨도 저장하고 싶다면 어디를 고쳐야 할까요? 클래스 안에서 고칠 곳 두 군데를 찾고, 캐릭터를 만드는 줄은 어떻게 바뀌어야 할지도 말해 보세요.

값만 묶는 것으로는 반쪽이다

그 값을 다루는 일도 함께 넣습니다.

캐릭터는 값만 갖고 가만히 있지 않습니다. 공격을 받으면 체력이 줄고, 체력이 0 이하가 되면 쓰러집니다. 이런 일들은 그 캐릭터의 체력을 알아야 할 수 있습니다. 그렇다면 값과 함께 넣어 두는 편이 자연스럽습니다.

class Character:
    def __init__(self, name, hp):
        self.name = name
        self.hp = hp

    def damage(self, amount):
        self.hp = self.hp - amount

    def is_alive(self):
        return self.hp > 0


hero = Character('용사', 120)
hero.damage(50)

print(hero.name, hero.hp, hero.is_alive())

용사 70 True가 나옵니다. 체력 120이던 용사가 50만큼 맞아 70이 되었고, 아직 살아 있습니다.

메서드(method)

클래스 안에 넣어 둔 함수입니다. 자기 속성을 갖고 일하는 함수라고 보면 됩니다.

함수와 메서드의 차이
호출하는 법 무엇을 갖고 일하는가
함수 len(scores) 전달받은 값만 갖고 일합니다. 자기 것이라 할 만한 값이 없습니다.
메서드 hero.damage(50) 전달받은 값에 더해 자기 속성을 갖고 일합니다. 누구의 체력을 깎을지 따로 말하지 않아도 됩니다.

hero.damage(50)에서 점 앞에 있는 hero가 매개변수 self로 들어갑니다. 괄호 안에는 50 하나만 적었는데 def damage(self, amount)에는 매개변수가 둘인 이유도 여기 있습니다. 첫 매개변수 self는 파이썬이 점 앞의 것으로 채워 줍니다. 「누구의」를 매번 적지 않아도 되게 만든 장치입니다.

값과 일을 함께 두면 무엇이 좋아지나

체력을 깎는 규칙이 바뀌었다고 해 봅시다. 방어력만큼 덜 깎이게 하려면? 값과 일이 흩어져 있으면 체력을 건드리는 코드를 전부 찾아다녀야 합니다. damage 안에 모아 두었다면 그 한 곳만 고치면 됩니다. 「체력은 이 클래스가 책임진다」고 맡을 곳을 정해 둔 셈입니다.

스스로 확인

체력을 회복하는 메서드를 하나 더 넣는다면 어떤 모양이 될까요? 무엇을 전달받고, 무엇을 바꾸고, 무엇을 반환할지 말해 보세요.

각자 자기 값을 갖는다는 것

그래서 하나를 건드려도 나머지는 그대로입니다.

같은 틀로 찍어 냈어도 각자 자기 값을 따로 갖습니다. 셋을 만들어 놓고 한 명만 공격해 보면 이것이 분명해집니다.

class Character:
    def __init__(self, name, hp):
        self.name = name
        self.hp = hp

    def damage(self, amount):
        self.hp = self.hp - amount


party = [Character('용사', 120), Character('마법사', 80), Character('궁수', 95)]
party[1].damage(30)

for member in party:
    print(member.name, member.hp)

마법사의 체력만 50으로 줄고 용사와 궁수는 그대로입니다. 그리고 하나 더 눈여겨볼 것이 있습니다. 인스턴스도 리스트에 저장할 수 있습니다. 한 캐릭터에 관한 값이 통째로 묶여 있으니 목록이 어긋날 걱정 없이 순서를 바꾸거나 지울 수 있습니다. 목록이 어긋나던 문제가 여기서 사라졌습니다.


우리가 쓰는 화면도 이렇게 만들어져 있다

이 구조는 게임에만 쓰이는 것이 아닙니다. 지금 보고 있는 화면의 버튼 하나하나를 떠올려 보세요.

버튼

자기 문구와 자기 색을 갖고, 눌렸을 때 할 일을 알고 있습니다.

연락처 항목

이름과 번호를 갖고, 전화 걸기를 할 줄 압니다.

알림

내용과 시각을 갖고, 사라지기를 할 줄 압니다.

셋 다 「자기 값 + 자기가 할 줄 아는 일」이라는 같은 모양입니다. 버튼이 수십 개여도 틀은 하나이고, 새 버튼을 만들 때는 문구와 색만 정해 주면 됩니다. 화면을 만드는 프로그램들이 이런 구조를 쓰는 이유가 여기 있습니다.


그렇다고 늘 클래스를 쓰는 것은 아니다

좋아 보인다고 무엇이든 클래스로 만들면 오히려 읽기 어려워집니다. 쓸 때와 그렇지 않은 때를 구별해야 합니다.

클래스를 쓸 때와 쓰지 않아도 되는 때
이런 상황이면 어떻게 왜
값 여러 개를 하나로 묶어 다루어야 한다 클래스로 따로 두면 어긋납니다. 목록이 어긋나던 그 문제입니다.
그 값을 다루는 일이 함께 있다 클래스로 값과 일을 한곳에 모아 두면 고칠 곳이 뚜렷해집니다.
같은 모양을 여럿 만들어야 한다 클래스로 틀을 한 번 만들어 두면 찍어 내기만 하면 됩니다.
값이 하나뿐이고 함께 다룰 일도 없다 변수로 충분 틀을 만드는 줄이 오히려 늘어나기만 합니다.

여기서는 틀 하나를 만드는 것까지만 봅니다

클래스에는 이 밖에도 여러 기능이 있습니다. 이미 만든 틀을 바탕으로 조금 다른 틀을 새로 만드는 방법 같은 것들입니다. 지금은 「값과 일을 하나로 묶는다」는 생각과, 틀 하나를 직접 만들어 쓰는 것까지 알아 두면 충분합니다.

스스로 확인

학교 도서관의 책을 클래스로 만든다면 어떤 값을 갖고 무엇을 할 줄 알아야 할까요? 각각 두 가지씩 들어 보세요.

핵심 정리

왜 묶는가

따로 두면 어긋난다

  • 목록을 나누어 두면 같은 인덱스가 같은 대상을 가리켜야 한다
  • 그 약속은 코드에 적혀 있지 않다
  • 어긋나도 오류가 나지 않는다

클래스와 인스턴스

class & instance

  • 클래스 — 무슨 값을 갖고 무슨 일을 하는지 정해 둔 틀. 값은 인스턴스마다 따로 갖는다
  • 인스턴스 — 그 틀로 찍어 낸 하나. 각자 자기 값을 갖는다
  • self는 지금 다루고 있는 바로 그 하나

속성과 메서드

값과 할 줄 아는 일

  • 속성 — 인스턴스가 갖는 값. hero.name처럼 점으로 꺼낸다
  • 메서드 — 자기 속성을 갖고 일하는 함수
  • 점 앞에 있는 것이 매개변수 self로 들어간다

언제 쓰나

구별해서 쓴다

  • 값을 하나로 묶어 다루어야 하고 함께 다룰 일이 있을 때
  • 같은 모양을 여럿 만들어야 할 때
  • 값이 하나이고 함께 다룰 일도 없으면 변수로 충분하다

확인 퀴즈

배운 내용을 확인해 봅시다.

Q1

클래스와 인스턴스의 관계를 바르게 말한 것은?

Q2

hero.damage(50)이라고 호출할 때 self에 해당하는 것은?

Q3

다음 중 굳이 클래스로 만들 이유가 가장 적은 것은?