![[OOP 02] 클래스와 객체](https://img.thenullpage.com/posts/6621/6621_1_caf0e2.webp)
02.1 설계도와 물건
클래스(class)는 설계도이고, 객체(object)는 그 설계도로 찍어낸 실제 물건이다. 같은 객체를 특히 어떤 클래스에서 나왔다는 점을 강조할 때 인스턴스(instance, 클래스로 만들어낸 개별 객체)라고 부른다. 붕어빵으로 비유하면 붕어빵 틀이 클래스, 그 틀로 구워 나온 붕어빵 하나하나가 객체이자 인스턴스다. 틀은 하나지만 붕어빵은 여러 개 나온다.
설계도만 있으면 아무 일도 일어나지 않는다. 설계도로 물건을 찍어낼 때 초기 상태를 정해줘야 한다. 그 일을 하는 특별한 메서드(method, 클래스 안에 정의된 함수)가 __init__이다. 밑줄 두 개로 감싼 이 이름을 생성자(constructor, 객체가 만들어질 때 자동으로 불려 초기값을 세팅하는 메서드)라 부른다. 객체를 만드는 순간 파이썬이 알아서 __init__을 호출한다.
02.2 클래스 정의와 인스턴스 생성
Dog라는 설계도를 만들고, 그 설계도로 서로 다른 강아지 두 마리를 찍어낸다. 설계도는 하나지만 나온 물건은 별개라는 점을 확인한다.
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
d1 = Dog("바둑이", 3)
d2 = Dog("초코", 5)
print(d1.name, d1.age)
print(d2.name, d2.age)
print(type(d1))
print(d1 is d2)
출력은 다음과 같다.
바둑이 3
초코 5
<class '__main__.Dog'>
False
Dog("바둑이", 3)이라고 쓰면 파이썬이 __init__을 부르면서 name에 바둑이, age에 3을 넣은 새 객체를 만든다. 같은 Dog 클래스로 만들었어도 d1과 d2는 서로 다른 객체다. 그래서 d1 is d2가 False다. 설계도는 하나, 물건은 여러 개라는 말이 이 False 한 줄에 들어 있다.
type(d1)이 클래스 '__main__.Dog'를 알려주는 것도 눈여겨본다. 객체는 자기가 어떤 클래스에서 나왔는지 안다. 이 정보 덕분에 나중에 여러 종류의 객체를 섞어 다룰 때 파이썬이 각 객체에 맞는 동작을 찾아 부를 수 있다. 지금은 d1과 d2가 독립된 두 물건이라는 것, 그리고 그 둘을 찍어낸 틀이 Dog 하나라는 것만 확실히 잡고 간다.
02.3 생성자 없는 클래스와 있는 클래스
생성자를 두지 않으면 속성(attribute, 객체가 지닌 데이터)을 객체를 만든 뒤 밖에서 하나씩 붙여야 한다. 이러면 속성을 빠뜨리기 쉽다. 생성자가 있으면 객체를 만드는 순간 필요한 값을 반드시 넘기게 강제된다.
# before: 생성자 없음, 밖에서 즉흥 부착
class PointBad:
pass
p = PointBad()
p.x = 1
p.y = 2
print("즉흥 부착:", p.x, p.y)
# after: 생성자로 초기화 강제
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
p2 = Point(1, 2)
print("생성자 초기화:", p2.x, p2.y)
두 방식 모두 같은 값을 찍는다.
즉흥 부착: 1 2
생성자 초기화: 1 2
결과는 같아도 안전성이 다르다. PointBad 방식은 p.y를 붙이는 것을 깜빡하면 나중에 p.y를 읽는 코드에서 AttributeError(속성 오류)가 난다. Point 방식은 Point(1, 2)로 만들 때 x와 y를 반드시 넘겨야 하므로, 객체가 태어나는 순간 항상 완전한 상태를 갖는다. 빠뜨림을 코드를 짜는 시점에 막는 것이다.
02.4 클래스 변수와 인스턴스 변수
원의 넓이를 구한다. 원주율은 모든 원이 똑같이 쓰는 값이고, 반지름은 원마다 다르다. 모두가 공유하는 값은 클래스 변수로, 객체마다 다른 값은 인스턴스 변수로 둔다.
class Circle:
pi = 3.14159 # 클래스 변수(모든 인스턴스가 공유)
def __init__(self, r):
self.r = r # 인스턴스 변수(객체마다 개별)
def area(self):
return Circle.pi * self.r * self.r
c1 = Circle(2)
c2 = Circle(3)
print(round(c1.area(), 2), round(c2.area(), 2))
print("공유값:", Circle.pi)
출력은 다음과 같다.
12.57 28.27
공유값: 3.14159
class 바로 아래에 쓴 pi는 클래스 변수라 모든 원이 같은 값을 공유한다. __init__ 안에서 self.을 붙여 만든 r은 인스턴스 변수라 원마다 따로 갖는다. 그래서 c1과 c2는 반지름은 다르지만 원주율은 하나를 같이 쓴다. 모두에게 똑같은 값이면 클래스 변수, 객체마다 달라야 하면 인스턴스 변수다.
02.5 가변 기본값의 함정
초보가 자주 밟는 함정 하나를 짚는다. 생성자 기본값으로 빈 리스트를 쓰면 그 리스트 하나를 모든 객체가 공유해 버그가 난다. 원인과 해결을 나란히 본다.
# before: 가변 기본값 공유 버그
class BasketBad:
def __init__(self, items=[]): # 위험: 이 [] 하나를 모두가 공유
self.items = items
b1 = BasketBad(); b1.items.append("사과")
b2 = BasketBad()
print("버그(공유됨):", b2.items)
# after: None으로 받고 안에서 새 리스트 생성
class Basket:
def __init__(self, items=None):
self.items = items if items is not None else []
b3 = Basket(); b3.items.append("사과")
b4 = Basket()
print("정상(개별):", b4.items)
출력이 문제를 그대로 보여준다.
버그(공유됨): ['사과']
정상(개별): []
기본값 []는 함수가 정의될 때 딱 한 번 만들어져 계속 재사용된다. 그래서 b1에 넣은 사과가 아무 상관 없어 보이는 b2에도 나타난다. 해결은 기본값을 None으로 두고, 안에서 매번 새 리스트를 만드는 것이다. 이러면 객체마다 독립된 리스트를 갖는다. 리스트나 딕셔너리를 기본값으로 쓰고 싶을 때는 항상 이 None 패턴을 쓴다.
02.6 정리
클래스는 설계도, 객체는 그 설계도로 찍은 물건이다. __init__은 객체가 태어날 때 자동으로 불려 초기 상태를 채우는 생성자이고, 이것이 있으면 필요한 값을 빠뜨리지 않게 강제할 수 있다. 모두가 공유하는 값은 클래스 변수로, 객체마다 다른 값은 self.을 붙인 인스턴스 변수로 둔다. 가변 기본값은 공유 버그를 부르니 None 패턴으로 피한다. 이 네 가지만 몸에 익혀도 클래스를 안전하게 정의하고 인스턴스를 원하는 만큼 찍어낼 수 있다.