분실 신고서
LOST-007
신고인
오케스트라 순회공연 운송 담당 문 실장
잃어버린 것
악기 케이스 하나에 '파손 주의' 스티커를 붙였는데, 같은 규격으로 만든 케이스 전부에 그 스티커가 붙어 있다.
해결 조건
스티커 목록이 실제로 어디에 있는지 찾고, 케이스마다 자기 스티커 목록을 갖게 한다.
하늘섬 국제공항 수하물 센터
도미란 반장의 책상 서랍에는 가방 규격서가 한 묶음 있습니다. 규격서 한 장으로 같은 모양의 가방을 몇 개든 찍어 낼 수 있습니다. "규격서도 종이 한 장, 가방도 하나하나. 둘 다 물건이야."
문 실장은 바이올린 케이스에 '파손 주의'를 붙였을 뿐인데, 첼로 케이스에도 같은 스티커가 보인다고 합니다. 반장은 케이스가 아니라 규격서를 펼쳐 봅니다. 스티커는 거기에 붙어 있었습니다.
class 문은 클래스 객체를 만들고 이름에 묶는 대입입니다(1교시의 def와 같습니다). 클래스도 타입을 가진 객체이고, 그 타입은 type입니다.
class Case:
size = "M"
def __init__(self, owner):
self.owner = owner
print(type(Case), isinstance(Case, type))
# <class 'type'> True
b = Case("cello")
print(type(b).__name__, type(b) is Case)
# Case True
print(vars(b))
# {'owner': 'cello'}
print([k for k in vars(Case) if not k.startswith("__")])
# ['size']
print(b.size, "size" in vars(b))
# M False| 무엇을 담나 | 보는 법 | |
|---|---|---|
| 클래스(규격서) | 클래스 본문에서 만든 이름: 클래스 속성, 메서드 | vars(Case), Case.__dict__ |
| 인스턴스(가방) | 그 가방만의 속성. 대개 __init__에서 self.x = ...로 | vars(b), b.__dict__ |
b의 __dict__에는 owner만 있습니다. 그런데도 b.size가 "M"으로 읽힙니다.__dict__를 먼저 보고, 없으면 클래스를 봅니다. 가방에 없으면 규격서를 펼쳐 보는 셈입니다.__dict__는 mappingproxy라는 읽기 전용 창입니다. 클래스 속성을 바꾸려면 Case.size = "L"처럼 클래스에 직접 대입합니다.신고인의 규격서입니다.
class Case:
stickers = []
def __init__(self, owner):
self.owner = owner
a = Case("violin")
b = Case("cello")
a.stickers.append("FRAGILE")
print(b.stickers, Case.stickers)
# ['FRAGILE'] ['FRAGILE']
print(a.stickers is b.stickers, "stickers" in vars(a))
# True Falsestickers = []는 클래스 본문에서 한 번 실행됩니다. 리스트는 규격서에 하나 붙어 있을 뿐입니다.a.stickers는 a의 __dict__에 없으니 클래스에서 찾고, append는 그 하나뿐인 리스트를 바꿉니다(가방 바꾸기).인스턴스마다 따로 가져야 하는 것은 __init__에서 self에 붙입니다. 아래에서 세 방식을 비교해 보세요.
태그 보드
단계 1 / 0
다음에 실행할 줄: 0
실습대를 준비하는 중…
방금 바뀐 것
봉인된 가방(불변)
세 번째 방식처럼 인스턴스에 같은 이름으로 대입하면, 클래스 속성은 그대로 두고 인스턴스 __dict__에 새 이름이 생깁니다. 그 인스턴스에서는 클래스 속성이 가려집니다(5교시의 내장 이름 가리기와 같은 모양입니다).
class Case:
size = "M"
a = Case()
b = Case()
a.size = "L"
print(a.size, b.size, vars(a))
# L M {'size': 'L'}
Case.size = "S"
print(a.size, b.size)
# L S
del a.size
print(a.size)
# Sb)에 곧바로 보입니다.del a.size로 인스턴스의 이름을 떼면 다시 클래스의 값이 보입니다.+=도 대입이라는 점을 잊지 마세요. 숫자 클래스 속성에 +=를 쓰면 인스턴스에 새 이름이 생깁니다.
class Gate:
count = 0
g = Gate()
g.count += 1
print(g.count, Gate.count, vars(g))
# 1 0 {'count': 1}g.count += 1은 g.count = g.count + 1입니다. 오른쪽은 클래스에서 0을 읽고, 왼쪽은 g의 __dict__에 1을 넣습니다. 모든 인스턴스가 함께 세는 숫자라면 Gate.count += 1처럼 클래스에 직접 대입해야 합니다.
핀의 안내
→ 클래스 본문에 리스트나 딕셔너리를 두면 모든 인스턴스가 함께 써요. 상수나 기본값처럼 정말 함께 쓸 것만 클래스에 두고, 가방마다 다른 것은 __init__ 에서 self 에 붙이세요.
b.x를 읽을 때 파이썬이 거치는 길을 검색대 흐름도로 봅니다. 이번 교시에서는 2단계(인스턴스 __dict__)와 4단계(클래스의 평범한 속성)만 보면 됩니다. 1·3·5단계는 10교시에서 다룹니다.
아래 b는 size를 클래스에도, 자기 __dict__에도 갖고 있습니다.
검색대 흐름도
1
class Case:
2
size = "M"
3
stickers = []
4
def __init__(self, owner):
5
self.owner = owner
6
def label(self):
7
return "case of " + self.owner
8
9
b = Case("cello")
10
b.size = "L"
b.
클래스에 데이터 디스크립터(__get__ + __set__)가 있나?
인스턴스 __dict__에 있나?
클래스에 비데이터 디스크립터(함수처럼 __get__만)가 있나?
클래스에 평범한 속성이 있나?
__getattr__가 있나?
AttributeError
결과: b. →
size는 클래스에도 있지만 2단계에서 먼저 찾으므로 'L'입니다. 결과 줄 괄호 안의 Case.size는 "클래스에도 같은 이름이 있다"는 표시입니다.label은 클래스에 있는 함수입니다. 함수는 3단계에 걸리는 특별한 객체라서, 꺼내면 그냥 함수가 아니라 bound method가 됩니다. 다음 절의 주제입니다.메서드는 클래스에 든 평범한 함수입니다. 인스턴스를 통해 꺼내면 파이썬이 함수와 인스턴스를 묶은 새 객체, 바운드 메서드를 만들어 줍니다.
class Bag:
def __init__(self, owner):
self.owner = owner
def label(self):
return "bag of " + self.owner
bag = Bag("kim")
print(bag.label is bag.label, bag.label == bag.label)
# False True
print(bag.label.__self__ is bag, bag.label.__func__ is Bag.label)
# True True
print(bag.label(), Bag.label(bag))
# bag of kim bag of kim
print(type(bag.label).__name__, type(Bag.label).__name__)
# method functionbag.label을 쓸 때마다 새 바운드 메서드가 생깁니다. 그래서 is는 False입니다. 같은 함수, 같은 인스턴스를 묶었으니 ==는 True입니다.__self__는 묶인 인스턴스, __func__는 클래스의 원래 함수입니다.bag.label()은 Bag.label(bag)과 같습니다. self는 바운드 메서드가 대신 채워 주는 첫 번째 인자일 뿐입니다.태그 보드
단계 1 / 0
다음에 실행할 줄: 0
실습대를 준비하는 중…
방금 바뀐 것
봉인된 가방(불변)
vars, getattr, setattr, hasattr (5분)속성 이름을 실행 중에 문자열로 정해야 할 때가 있습니다. 설정 파일의 키, 화면에서 고른 열 이름 같은 경우입니다.
class Bag:
def __init__(self, owner):
self.owner = owner
bag = Bag("kim")
setattr(bag, "color", "red")
print(getattr(bag, "color"), hasattr(bag, "weight"), getattr(bag, "weight", 0))
# red False 0
print(vars(bag))
# {'owner': 'kim', 'color': 'red'}
vars(bag)["tag"] = "T1"
print(bag.tag, vars(bag) is bag.__dict__)
# T1 True
bag.weight
# AttributeError: 'Bag' object has no attribute 'weight'setattr(obj, "x", v)는 obj.x = v, getattr(obj, "x")는 obj.x와 같습니다. getattr의 세 번째 인자는 없을 때 돌려줄 기본값입니다.hasattr는 실제로 getattr을 해 보고 AttributeError가 나는지로 판단합니다.vars()는 스냅샷이 아니라 진짜 __dict__입니다. 5교시의 함수 안 locals()와 다릅니다.bag.colour = ...)도 조용히 새 속성이 됩니다.__slots__와 그 밖의 새 기능 (5분)__slots__에 속성 이름을 적어 두면, 인스턴스에 __dict__가 생기지 않고 적힌 이름만 쓸 수 있습니다. CPython에서는 인스턴스마다 딕셔너리가 없어 메모리도 덜 씁니다.
class Slim:
__slots__ = ("owner",)
def __init__(self, owner):
self.owner = owner
s = Slim("kim")
print(s.owner, hasattr(s, "__dict__"))
# kim False
s.color = "red"
# AttributeError: 'Slim' object has no attribute 'color' and no __dict__ for setting new attributes3.13부터 클래스에는 __static_attributes__가 있습니다. 클래스의 메서드 안에서 self.이름 = ...으로 값을 넣은 이름들을 정렬해 모아 둡니다.
class Trunk:
def __init__(self, owner):
self.owner = owner
self.tags = []
def pack(self):
self.weight = 10
print(Trunk.__static_attributes__)
# ('owner', 'tags', 'weight')마지막으로 3.14의 어노테이션 지연 평가(PEP 649, 749)입니다. 클래스 본문의 어노테이션은 클래스를 만들 때 계산하지 않고, __annotations__를 처음 읽을 때 계산합니다. 그래서 아직 정의하지 않은 이름을 어노테이션에 써도 클래스가 만들어집니다. 3.13까지는 friend: Missing 줄에서 NameError가 났습니다.
class Partner:
friend: Missing
print("created", Partner.__name__)
# created Partner
class Missing:
pass
print(Partner.__annotations__)
# {'friend': <class '__main__.Missing'>}스티커 목록을 규격서에서 떼어 케이스마다 붙입니다. 함께 쓸 값(규격 크기)은 클래스에 남겨 둡니다.
class Case:
size = "M"
def __init__(self, owner):
self.owner = owner
self.stickers = []
a = Case("violin")
b = Case("cello")
a.stickers.append("FRAGILE")
print(a.stickers, b.stickers)
# ['FRAGILE'] []
print(vars(a))
# {'owner': 'violin', 'stickers': ['FRAGILE']}
print(Case.__static_attributes__)
# ('owner', 'stickers')이제 '파손 주의'는 바이올린 케이스에만 붙습니다. 문 실장이 돌아간 뒤, 핀이 13번 컨베이어의 유령 가방 기록을 화면에 띄웁니다. 두 가방은 같은 규격으로 만들어졌고, 한쪽의 partner 속성은 다른 쪽을, 다른 쪽의 partner는 다시 이쪽을 가리킵니다. 반장은 그 줄에 펜으로 동그라미를 칩니다.
종결 보고 — LOST-007. 스티커 리스트가 클래스 속성이라 모든 케이스가 같은 리스트를 공유. __init__에서 self.stickers = []로 케이스마다 새 리스트를 만들어 해결.
Case.stickers = []를 클래스에 그대로 두고, a.stickers += ["FRAGILE"]을 실행하면 b.stickers와 vars(a)는 어떻게 될까요? (힌트: 2교시의 +=와 7-2의 +=가 동시에 일어납니다.)__slots__ = ("owner",)인 클래스에 클래스 속성 size = "M"을 두면, 인스턴스에서 s.size를 읽을 수 있을까요? s.size = "L"은요?getattr로 ["owner", "color", "weight"] 세 속성을 차례로 꺼내 출력하되, 없는 속성은 "-"로 출력하는 반복문을 써 보세요.반장의 업무 수첩
클래스도 객체. type(Case) 는 type. class 문도 이름을 묶는 대입이다.
인스턴스는 자기 __dict__를 갖는다. 읽기는 인스턴스 → 클래스 순서.
클래스 속성은 모든 인스턴스가 공유. 가변 객체를 두면 한 곳의 변경이 전부에 보인다.
인스턴스에 같은 이름으로 대입하면 클래스 속성을 가린다. += 도 대입이다.
bag.m 은 꺼낼 때마다 새 바운드 메서드(is 는 False, == 는 True). __self__, __func__.
getattr / setattr / hasattr 로 이름을 문자열로 다룬다. vars(obj) 는 진짜 __dict__.
__slots__ 는 새 속성을 막는다. __static_attributes__(3.13), 어노테이션 지연 평가(3.14).
새 교시·새 강의 소식 받기
이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.
R
ReflexKR
눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.
ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.
© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
질문과 답변
이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.