따로 든 목록은 언젠가 어긋난다
그리고 어긋나도 오류는 나지 않습니다.
간단한 게임을 만든다고 해 봅시다. 캐릭터마다 이름 · 체력 · 레벨을 저장해야 합니다. 리스트만 쓴다면 목록 세 개를 만들게 됩니다.
names = ['용사', '마법사', '궁수']
hps = [120, 80, 95]
levels = [5, 4, 6]
print(names[1], hps[1], levels[1])마법사 80 4가 나옵니다. 잘 실행됩니다. 그런데 이 방식에는 깨지기 쉬운 약속이 하나 숨어 있습니다. 세 목록의 같은 인덱스가 모두 같은 캐릭터를 가리켜야 한다는 약속입니다. 코드 어디에도 적혀 있지 않고, 만든 사람이 기억하고 있을 뿐입니다.
마법사가 게임에서 빠진다면
세 목록에서 각각 인덱스 1의 값을 지워야 합니다.
그런데 levels에서 지우는 것을 빠뜨렸다고 해 봅시다.
그러면 그때부터 궁수의 이름에 마법사의 레벨이 붙습니다.
오류는 나지 않습니다. 프로그램은 멀쩡히 실행되고, 숫자만 조용히 틀립니다.
목록이 셋이 아니라 열 개라면 이런 일이 일어날 확률은 훨씬 높아집니다.
문제의 뿌리는 한 캐릭터에 관한 값들이 세 곳에 흩어져 있다는 것입니다. 그렇다면 답도 분명합니다. 한 캐릭터의 값은 하나로 묶어 두면 됩니다. 그러면 캐릭터를 지울 때 그 묶음 하나만 지우면 되고, 어긋날 일이 없습니다.
학급 명단을 이름 목록 · 번호 목록 · 연락처 목록 셋으로 나누어 관리한다면 어떤 사고가 날 수 있을까요? 한 가지를 들어 보세요.
틀을 하나 만들어 두고 여럿을 찍어 낸다
틀은 하나, 찍어 낸 것은 여럿입니다.
캐릭터를 묶는다고 할 때, 캐릭터마다 어떤 값을 갖는지는 모두 같습니다. 용사도 마법사도 이름과 체력을 갖습니다. 값만 다를 뿐입니다. 그렇다면 「캐릭터란 이름과 체력을 갖는 것」이라는 틀을 한 번만 정해 두고, 그 틀로 캐릭터를 찍어 내면 됩니다.
클래스(class)
어떤 것이 무슨 값을 갖는지 정해 둔 틀입니다. 무슨 일을 할 줄 아는지도 함께 정해 둘 수 있습니다. 틀에는 캐릭터마다 다른 값이 들어 있지 않습니다.
인스턴스(instance)
그 틀로 실제로 만들어 낸 것 하나하나입니다. 각자 자기 값을 갖습니다.
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로 들어간다
언제 쓰나
구별해서 쓴다
- 값을 하나로 묶어 다루어야 하고 함께 다룰 일이 있을 때
- 같은 모양을 여럿 만들어야 할 때
- 값이 하나이고 함께 다룰 일도 없으면 변수로 충분하다
확인 퀴즈
배운 내용을 확인해 봅시다.
클래스와 인스턴스의 관계를 바르게 말한 것은?
hero.damage(50)이라고 호출할 때 self에 해당하는 것은?
다음 중 굳이 클래스로 만들 이유가 가장 적은 것은?