프로그래밍

클래스와 인스턴스

값은 딕셔너리로 묶을 수 있습니다. 그런데 그 값으로 하는 일은 아직 밖에 있습니다.
값과 그 값으로 하는 일을 한곳에 묶는 방법을 배웁니다.

학습 목표

  1. 클래스와 인스턴스의 관계를 설명할 수 있다.
  2. 생성자 __init__으로 인스턴스를 만들 때 속성의 값을 정할 수 있다.
  3. self가 무엇을 가리키는지 알고 속성을 다룰 수 있다.
  4. 메서드를 정의해 값과 그 값으로 하는 일을 한 클래스에 묶을 수 있다.
  5. 다룰 값과 하는 일에 따라 딕셔너리와 클래스 중 맞는 쪽을 고를 수 있다.

값만 묶으면 반쪽이다

딕셔너리로 묶어도 남는 문제가 있습니다.

학생 한 명의 값을 딕셔너리로 묶으면 이름과 점수를 리스트 여러 개에 나누어 저장할 때의 문제는 해결됩니다. 이름과 점수가 한 딕셔너리에 함께 저장되기 때문입니다.

a = {"name": "학생 A", "kor": 90, "eng": 65}
b = {"name": "학생 B", "kor": 85, "eng": 80}


def average(student):               # 하는 일은 「밖에」 따로 있다
    return round((student["kor"] + student["eng"]) / 2, 1)


print(average(a))       # 77.5
print(average(b))       # 82.5

c = {"name": "학생 C", "kor": 40}   # eng를 빠뜨렸다. 오류는 「지금」 나지 않는다
# print(average(c))                 # 호출할 때가 되어서야 KeyError로 멈춘다
값 따로, 하는 일 따로 딕셔너리 "kor": 90 "eng": 65 함수 average(student) 키를 빠뜨려도 「호출할 때까지」 모른다 한곳에 함께 class Student self.kor = 90 self.eng = 65 average(self) 빠뜨리면 「만들 때」 오류가 난다

그런데 그 값으로 하는 일인 average는 딕셔너리 밖에 있습니다. 값과 함수가 따로 있으면 두 가지 문제가 생깁니다.

어떤 키가 있어야 하는지 어디에도 정해져 있지 않습니다

c는 eng 키가 빠진 채로 아무 오류 없이 만들어집니다. 이 잘못은 average(c)를 호출할 때가 되어서야 드러납니다. 딕셔너리를 만든 곳과 오류가 난 곳이 멀리 떨어져 있어서 원인을 찾기 어렵습니다.

함수가 어떤 딕셔너리를 전달받아야 하는지도 드러나지 않습니다

average(student)는 함수 이름과 매개변수만 봐서는 어떤 키를 쓰는지 알 수 없습니다. 함수 본문을 읽어야 kor과 eng가 필요하다는 것을 알 수 있습니다.

스스로 확인

딕셔너리를 만들 때 eng 대신 english라는 키를 썼다면 그 잘못을 언제 알게 될까요?

틀을 만들고 찍어 내기

어떤 값을 갖는지 한곳에 정해 둡니다.

어떤 값을 가져야 하는지 미리 정해 두면 값을 빠뜨리는 문제를 막을 수 있습니다.

class Student:
    def __init__(self, name, kor, eng):     # 인스턴스를 만들 때마다 「한 번」 호출된다
        self.name = name                    # self는 「지금 만드는 그 인스턴스」다
        self.kor = kor
        self.eng = eng


a = Student("학생 A", 90, 65)       # 인스턴스를 하나 만든다
print(a.name)                       # 학생 A
print(a.kor + a.eng)                # 155

# b = Student("학생 B", 85)         # TypeError. 인자를 빠뜨리면 「만들 때」 오류가 난다

클래스(class)

같은 종류의 대상이 어떤 값을 갖고 그 값으로 어떤 일을 하는지 정해 둔 틀입니다. 하는 일을 정하는 방법은 「하는 일까지 함께 두기」 절에서 봅니다. 여기서 만드는 클래스에는 값이 들어 있지 않고, 값은 인스턴스마다 따로 저장합니다. 클래스 자체에 값을 두는 방법도 있지만 이번 시간에는 인스턴스가 갖는 값만 다룹니다.

인스턴스(instance)

클래스로 실제로 만들어 낸 대상 하나하나입니다. 인스턴스마다 자기 값을 따로 갖습니다.

틀은 하나, 찍어 낸 것은 여럿 class Student name · kor · eng 어떤 값을 갖는지만 정한다 학생 A kor = 90 eng = 65 학생 B kor = 85 eng = 80 학생 C kor = 40 eng = 55 인스턴스 셋 — 각자 「자기 값」을 갖는다
class Student:
    def __init__(self, name, kor, eng):
        self.name = name
        self.kor = kor
        self.eng = eng


a = Student("학생 A", 90, 65)
b = Student("학생 B", 85, 80)

a.kor = 95                  # a의 값만 바뀐다
print(a.kor, b.kor)         # 95 85    인스턴스마다 자기 값을 갖는다
print(a is b)               # False    같은 클래스로 만들었어도 서로 다른 인스턴스다

같은 클래스로 만들어도 서로 다른 인스턴스입니다

a.kor = 95는 a의 값만 바꿉니다. b는 그대로입니다. 클래스는 구조만 정할 뿐 값은 인스턴스마다 따로 있습니다.

스스로 확인

Student 클래스 자체에는 kor 값이 있을까요, 없을까요?

__init__과 self

인스턴스를 만들 때마다 한 번 실행되는 함수입니다.

class Student:라고 쓰기만 해서는 인스턴스에 값이 저장되지 않습니다. 인스턴스의 값을 저장하는 함수를 클래스 안에 따로 정의합니다.

만들 때 「한 번」 실행되어 속성에 값을 저장한다 Student("학생 A", 90, 65) def __init__(self, name, kor, eng): self.name = name self.kor = kor self self 학생 A kor = 90 eng = 65 self는 「지금 만들고 있는 바로 그 인스턴스」다

__init__

인스턴스를 새로 만들 때마다 자동으로 한 번 호출되는 함수입니다. 인스턴스의 값을 처음 저장하는 단계라고 보면 됩니다. 이런 역할을 하는 __init__을 생성자(constructor)라고 합니다.

self

지금 다루고 있는 인스턴스 자신을 가리키는 매개변수입니다. __init__에서는 지금 만들고 있는 인스턴스를 가리키므로, self.kor는 「이 학생의 kor」입니다. self는 파이썬의 예약어가 아니라, 클래스 안에 정의한 함수의 첫 번째 매개변수에 관례로 붙이는 이름입니다.

name · kor처럼 인스턴스에 속한 변수를 속성(attribute)이라고 하고, self.kor · a.name처럼 점을 찍어 사용합니다. 함수 안에서 만든 지역 변수는 함수가 끝나면 사라지지만, 속성은 인스턴스에 저장되므로 __init__이 끝난 뒤에도 a.name으로 꺼낼 수 있습니다.

값을 빠뜨리면 만들 때 오류가 납니다

Student("학생 B", 85)는 __init__에 전달할 인자가 하나 모자라므로 그 줄에서 TypeError가 발생합니다. 딕셔너리를 쓸 때는 한참 뒤에야 드러나던 잘못이 인스턴스를 만드는 순간 드러나는 것입니다.

스스로 확인

self.name = name에서 self.name과 name은 서로 다른 것입니다. 각각 무엇일까요?

하는 일까지 함께 두기

값과 그 값으로 하는 일이 한 클래스 안에 있습니다.

이제 average를 클래스 안으로 옮깁니다.

class Student:
    def __init__(self, name, kor, eng):
        self.name = name
        self.kor = kor
        self.eng = eng

    def average(self):                          # 값과 「하는 일」을 함께 둔다
        return round((self.kor + self.eng) / 2, 1)

    def passed(self):
        return self.average() >= 60             # 메서드 안에서 다른 메서드를 호출할 수도 있다


a = Student("학생 A", 90, 65)
print(a.average())      # 77.5      괄호 안이 비어 있다 — self에는 a가 자동으로 전달된다
print(a.passed())       # True

메서드(method)

클래스 안에 정의한 함수입니다. 첫 번째 매개변수 self로 그 인스턴스의 속성을 사용하는 함수라고 보면 됩니다. 생성자 __init__도 메서드입니다. 앞에서 「값에 딸린 함수」로 배운 문자열의 upper나 리스트의 append도 str · list 클래스에 정의된 메서드이고, 문자열 값과 리스트 값은 각각 그 클래스의 인스턴스입니다.

괄호 안이 비어 있는데 어떻게 값을 아는가 a.average() Student.average(a) 점 앞의 인스턴스가 self에 「자동으로」 전달된다 a kor = 90 eng = 65 b kor = 85 eng = 80 a.average() → 77.5 b.average() → 82.5 같은 메서드도 「어느 인스턴스로 호출했나」에 따라 결과가 다르다
class Student:
    def __init__(self, name, kor, eng):
        self.name = name
        self.kor = kor
        self.eng = eng

    def average(self):
        return round((self.kor + self.eng) / 2, 1)


a = Student("학생 A", 90, 65)
b = Student("학생 B", 85, 80)

print(a.average())              # 77.5   self에 a가 전달된다
print(b.average())              # 82.5   self에 b가 전달된다
print(Student.average(a))       # 77.5   a.average()는 이렇게 실행된다

괄호가 비어 있는데도 값을 압니다

a.average()는 Student.average(a)와 같이 실행됩니다. 점 앞의 인스턴스 a가 self에 자동으로 전달되므로 괄호 안에 인자를 쓰지 않아도 됩니다.

딕셔너리 + 함수 클래스
값 묶기 된다 된다
하는 일 밖에 따로 안에 함께
값을 빠뜨리면 사용할 때 오류가 난다 만들 때 오류가 난다
호출 방법 average(a) a.average()
알맞은 경우 값만 잠깐 묶을 때 값과 하는 일을 늘 함께 쓸 때

딕셔너리가 부족한 것은 아닙니다

값을 잠깐 묶어 전달하는 경우에는 딕셔너리가 더 간단합니다. 클래스가 알맞은 것은 같은 값으로 하는 일이 여럿이고, 값과 그 일을 늘 함께 쓸 때입니다.

배운 것을 모으면 파일을 읽어 인스턴스를 만들고 결과를 출력하는 프로그램이 됩니다. 성적.csv는 한 줄에 「이름,국어,영어」를 쓴 파일이고, 여기서는 학생 A·B 두 줄 끝에 학생 C,40,55 줄을 더해 세 줄로 두었습니다.

# 성적.csv 한 줄은 「이름,국어,영어」다. 학생 A·B 두 줄에 「학생 C,40,55」 줄을 더한 경우다

class Student:
    def __init__(self, name, kor, eng):
        self.name = name
        self.kor = kor
        self.eng = eng

    def average(self):
        return round((self.kor + self.eng) / 2, 1)

    def report(self):
        mark = "통과" if self.average() >= 60 else "재응시"
        return f"{self.name}: 평균 {self.average()} ({mark})"


students = []
with open("성적.csv", "r", encoding="utf-8") as f:
    for line in f:
        values = line.strip().split(",")
        students.append(Student(values[0], int(values[1]), int(values[2])))

for s in students:
    print(s.report())
# 학생 A: 평균 77.5 (통과)
# 학생 B: 평균 82.5 (통과)
# 학생 C: 평균 47.5 (재응시)

report는 같은 인스턴스의 average를 호출합니다. 메서드 안에서 다른 메서드를 호출할 때도 self.를 붙입니다. 「이 학생의 평균」이라는 뜻이기 때문입니다.

스스로 확인

report 안에서 average()라고만 쓰면 어떻게 될까요? self.가 왜 필요한지 설명해 보세요.

핵심 정리

클래스와 인스턴스는 다르다

클래스는 어떤 값을 갖고 어떤 일을 하는지 정해 둔 틀이고, 인스턴스는 그 클래스로 만든 대상 하나하나다. 속성 값은 인스턴스마다 따로 있다.

생성자 __init__은 속성에 처음 값을 저장한다

인스턴스를 만들 때마다 자동으로 한 번 호출된다. 필요한 인자를 빠뜨리면 인스턴스를 만드는 그 줄에서 오류가 난다.

self는 인스턴스 자신

self.kor는 「이 학생의 kor」이다. a.average()는 Student.average(a)와 같이 실행된다.

메서드는 인스턴스의 속성을 사용한다

괄호가 비어 있어도 되는 것은 점 앞의 인스턴스가 self에 전달되기 때문이다. 같은 메서드라도 어느 인스턴스로 호출했는가에 따라 결과가 다르다.

선택 기준은 「값과 하는 일을 늘 함께 쓰는가」

값만 잠깐 묶는 경우에는 딕셔너리가 간단하다. 같은 값으로 하는 일이 여럿이고 값과 그 일을 늘 함께 쓴다면 클래스가 알맞다.

확인 퀴즈

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

1. a = Student("학생 A", 90, 65)에서 a는?

2. a.average()의 괄호가 비어 있어도 되는 이유는?

3. a.kor = 95를 실행하면?