분실 신고서
LOST-010
신고인
계량 시스템 유지보수 기사 오 실장
잃어버린 것
저울 기록. 무게 검사를 붙였는데도 -10kg짜리 가방이 기록에 올라 있다.
해결 조건
검사를 우회한 경로를 찾고, 어느 경로로 넣어도 검색대를 지나게 한다.
하늘섬 국제공항 수하물 센터
오 실장은 음수 무게를 막으려고 Bag을 만들 때 무게를 검사하게 했습니다. 그런데 기록에는 -10kg짜리 가방이 있습니다. 도미란 반장이 출입 기록을 넘겨 봅니다. "정문에 검색대를 세웠는데, 옆문으로 들어왔군."
오늘은 obj.x를 읽고 쓰는 순간 파이썬이 거치는 검색대를 봅니다. 7교시에서 "인스턴스 → 클래스 순서"라고 했던 읽기 규칙의 전체 모습이기도 합니다.
@property: 속성에 검색대 세우기 (9분)신고인의 코드입니다. 검사는 __init__에만 있습니다.
class Bag:
def __init__(self, kg):
if kg < 0:
raise ValueError("negative weight")
self.kg = kg
b = Bag(20)
b.kg -= 30
print(b.kg)
# -10만들 때는 검사를 지났지만, 그 뒤의 b.kg -= 30은 인스턴스 __dict__에 값을 바로 씁니다. 옆문입니다. 속성 쓰기 자체에 검사를 걸려면 @property를 씁니다.
class Bag:
def __init__(self, kg):
self.kg = kg
@property
def kg(self):
return self._kg
@kg.setter
def kg(self, value):
if value < 0:
raise ValueError(f"negative weight: {value}")
self._kg = value
b = Bag(20)
print(b.kg, vars(b))
# 20 {'_kg': 20}
b.kg -= 30
# ValueError: negative weight: -10b.kg를 읽으면 첫 번째 kg 함수가, b.kg = v를 쓰면 @kg.setter 함수가 불립니다. 겉으로는 평범한 속성처럼 씁니다.__init__의 self.kg = kg도 setter를 지납니다. 검사를 한 곳에만 두면 됩니다._kg에 들어 있습니다. kg라는 이름은 인스턴스가 아니라 클래스에 있는 property 객체입니다.setter를 주지 않으면 읽기 전용 속성이 됩니다. 다른 속성에서 계산하는 값에 알맞습니다.
class Box:
def __init__(self, w, h):
self.w, self.h = w, h
@property
def area(self):
return self.w * self.h
x = Box(3, 4)
print(x.area, type(Box.__dict__["area"]).__name__)
# 12 property
x.area = 5
# AttributeError: property 'area' of 'Box' object has no setter핀의 안내
→ _kg 처럼 밑줄 하나는 '건드리지 마세요'라는 약속일 뿐 잠금이 아니에요. b._kg = -5 는 막을 수 없어요. 검색대는 정상 경로(b.kg = …)를 지키는 장치예요.
신고인의 가방에는 무게(kg)뿐 아니라 길이(cm)도 있습니다. 속성마다 property를 복사해 붙이면 검사 코드가 늘어납니다. 검색대 하나를 만들어 여러 속성에 세울 수 있습니다. 그것이 디스크립터입니다.
디스크립터는 __get__, __set__(또는 __delete__) 중 하나 이상을 가진 객체입니다. 클래스 속성으로 두면, 인스턴스에서 그 이름을 읽고 쓸 때 파이썬이 그 메서드를 부릅니다.
class Positive:
def __set_name__(self, owner, name):
print(f"set_name: {owner.__name__}.{name}")
self.name = name
def __get__(self, obj, owner=None):
if obj is None:
return self
return obj.__dict__[self.name]
def __set__(self, obj, value):
if value < 0:
raise ValueError(f"{self.name} must be >= 0: {value}")
obj.__dict__[self.name] = value
class Bag:
kg = Positive()
cm = Positive()
def __init__(self, kg, cm):
self.kg, self.cm = kg, cm
# set_name: Bag.kg
# set_name: Bag.cm
b = Bag(20, 55)
print(b.kg, b.cm, vars(b))
# 20 55 {'kg': 20, 'cm': 55}
print(type(Bag.kg).__name__)
# Positive
b.cm = -1
# ValueError: cm must be >= 0: -1| 이럴 때 | 파이썬이 부르는 것 |
|---|---|
| 클래스를 다 만들었을 때 | Positive.__set_name__(검색대, Bag, "kg") |
b.kg를 읽을 때 | Positive.__get__(검색대, b, Bag) |
Bag.kg를 읽을 때 | Positive.__get__(검색대, None, Bag) |
b.kg = 20을 쓸 때 | Positive.__set__(검색대, b, 20) |
__set_name__(3.6)은 클래스 문이 끝나는 순간, 자기가 붙은 이름을 알려 줍니다. 그래서 같은 Positive를 kg에도 cm에도 세울 수 있습니다.__dict__에 적습니다."kg"로 __dict__에 넣었는데도, 읽을 때 __dict__가 아니라 검색대를 지나갑니다. 이 순서가 10-4의 주제입니다.@property도 파이썬이 미리 만들어 둔 디스크립터입니다.태그 보드
단계 1 / 0
다음에 실행할 줄: 0
실습대를 준비하는 중…
방금 바뀐 것
봉인된 가방(불변)
7교시에서 bag.m is bag.m이 False인 것을 봤습니다. 이유는 함수도 디스크립터이기 때문입니다. 함수는 __get__만 있고 __set__이 없는 비데이터 디스크립터입니다.
class Bag:
def weigh(self):
return 20
b = Bag()
f = Bag.__dict__["weigh"]
print(type(f).__name__, hasattr(f, "__get__"), hasattr(f, "__set__"))
# function True False
m = f.__get__(b, Bag)
print(type(m).__name__, m.__self__ is b, m())
# method True 20
print(b.weigh is b.weigh, b.weigh == b.weigh, Bag.weigh is f)
# False True Trueb.weigh를 읽을 때마다 파이썬은 f.__get__(b, Bag)을 불러 새 바운드 메서드를 만듭니다. 함수와 b를 함께 묶은 작은 객체입니다. 그래서 is는 False, ==는 True입니다. b.weigh()가 self 없이 불리는 것도 이 묶음 덕분입니다. 클래스에서 읽으면(Bag.weigh) 묶을 인스턴스가 없어 함수가 그대로 나옵니다.
staticmethod와 classmethod는 __get__이 무엇을 묶을지 바꾼 디스크립터입니다.
class Bag:
@staticmethod
def kg_to_lb(kg):
return round(kg * 2.2046, 1)
@classmethod
def empty(cls):
return cls()
class Suitcase(Bag):
pass
print(Bag.kg_to_lb(20), Bag().kg_to_lb(20))
# 44.1 44.1
print(type(Suitcase.empty()).__name__, Suitcase.empty.__self__ is Suitcase)
# Suitcase True
print(type(Bag.__dict__["empty"]).__name__)
# classmethod__get__이 묶는 것 | 첫 인자 | |
|---|---|---|
| 보통 함수 | 인스턴스 | self |
staticmethod | 아무것도 | 없음 |
classmethod | 클래스(type(obj)) | cls |
classmethod는 부른 쪽의 클래스를 받으므로 Suitcase.empty()가 Suitcase를 만듭니다. 상속해도 맞는 규격으로 만드는 "대체 생성자"에 씁니다.
obj.x를 읽을 때 파이썬(정확히는 object.__getattribute__)은 이 순서로 봅니다.
| 순서 | 어디를 보나 | 예 |
|---|---|---|
| 1 | 클래스(MRO)에 데이터 디스크립터(__set__이나 __delete__가 있는 것) | property, Positive |
| 2 | 인스턴스 __dict__ | b.label = "ICN" |
| 3 | 클래스에 비데이터 디스크립터(__get__만) | 함수, staticmethod, classmethod |
| 4 | 클래스의 평범한 속성 | kind = "bag" |
| 5 | __getattr__(있으면) | |
| 6 | AttributeError |
데이터 디스크립터가 인스턴스 __dict__보다 앞이라, 검색대는 가방 안의 값보다 먼저 불립니다. 10-2의 Positive가 같은 이름으로 __dict__에 값을 넣어도 괜찮았던 이유입니다. 반대로 함수는 __dict__보다 뒤라서 인스턴스에 같은 이름을 쓰면 메서드가 가려집니다.
아래 흐름도에서 속성을 하나씩 골라 보세요. 몇 번 단계에서 답이 나오는지 보여 줍니다.
검색대 흐름도
1
class Positive:
2
def __set_name__(self, owner, name):
3
self.name = name
4
def __get__(self, obj, owner):
5
return obj.__dict__[self.name]
6
def __set__(self, obj, value):
7
obj.__dict__[self.name] = value
8
class Bag:
9
kg = Positive()
10
kind = "bag"
11
def weigh(self):
12
return self.kg
13
def __repr__(self):
14
return "Bag()"
15
b = Bag()
16
b.kg = 20
17
b.label = "ICN"
b.
클래스에 데이터 디스크립터(__get__ + __set__)가 있나?
인스턴스 __dict__에 있나?
클래스에 비데이터 디스크립터(함수처럼 __get__만)가 있나?
클래스에 평범한 속성이 있나?
__getattr__가 있나?
AttributeError
결과: b. →
이번에는 같은 이름을 일부러 인스턴스 __dict__에 넣어 봅니다. 마지막 줄은 검색대를 피하려고 __dict__에 직접 쓴 "옆문"입니다.
검색대 흐름도
1
class Bag:
2
kind = "bag"
3
def weigh(self):
4
return 20
5
@property
6
def tag(self):
7
return "T-" + self.kind
8
def __getattr__(self, name):
9
return f"<no {name}>"
10
b = Bag()
11
b.weigh = "shadowed"
12
b.kind = "suitcase"
13
b.__dict__["tag"] = "fake"
b.
클래스에 데이터 디스크립터(__get__ + __set__)가 있나?
인스턴스 __dict__에 있나?
클래스에 비데이터 디스크립터(함수처럼 __get__만)가 있나?
클래스에 평범한 속성이 있나?
__getattr__가 있나?
AttributeError
결과: b. →
property도 데이터 디스크립터입니다. property는 언제나 __set__을 갖고 있고, setter가 없으면 그 안에서 AttributeError를 냅니다. 그래서 __dict__로 가릴 수 없습니다.b.x = v)의 순서는 더 짧습니다. 클래스에 __set__이 있는 디스크립터가 있으면 그것을 부르고, 없으면 인스턴스 __dict__에 씁니다.핀의 안내
→ 순서를 한 줄로 외우면 '데이터 디스크립터 > 인스턴스 __dict__ > 함수 같은 비데이터 디스크립터 > 클래스 속성 > __getattr__'이에요.
__getattr__, __getattribute__, 그리고 type (5분)이름이 비슷한 두 손잡이는 불리는 때가 다릅니다.
__getattribute__: 모든 속성 읽기에서 언제나 불립니다. 10-4의 순서 전체가 object.__getattribute__ 안에 들어 있습니다. 직접 덮어쓰면 무한 재귀에 빠지기 쉬워 거의 쓰지 않습니다.__getattr__: 위 순서에서 찾지 못했을 때만 불리는 마지막 구조대입니다.class Loud:
def __init__(self):
self.kg = 20
def __getattribute__(self, name):
print("getattribute", name)
return super().__getattribute__(name)
def __getattr__(self, name):
print("getattr", name)
return None
x = Loud()
v = x.kg
# getattribute kg
v = x.color
# getattribute color
# getattr color마지막으로, 클래스도 객체라면 클래스의 타입은 무엇일까요? type입니다. class 문은 사실 type(이름, 부모들, 속성 dict)를 부르는 것과 같습니다.
class Bag:
kind = "bag"
print(type(Bag()).__name__, type(Bag), type(type))
# Bag <class 'type'> <class 'type'>
Dyn = type("Dyn", (Bag,), {"kind": "dyn"})
print(Dyn.kind, [c.__name__ for c in Dyn.__mro__])
# dyn ['Dyn', 'Bag', 'object']type을 물려받아 클래스가 만들어지는 과정 자체를 바꾸는 것을 메타클래스라고 합니다. 대부분의 일은 __init_subclass__(9교시)와 디스크립터로 충분하니, 이 강의에서는 이름만 기억해 두세요.
검사를 Positive 검색대로 옮기면, 만들 때도, 고칠 때도, -=로 줄일 때도 모두 같은 검색대를 지납니다.
class Positive:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, owner=None):
return self if obj is None else obj.__dict__[self.name]
def __set__(self, obj, value):
if value < 0:
raise ValueError(f"{self.name} must be >= 0: {value}")
obj.__dict__[self.name] = value
class Bag:
kg = Positive()
cm = Positive()
def __init__(self, kg, cm):
self.kg, self.cm = kg, cm
b = Bag(20, 55)
for change in (lambda: setattr(b, "kg", -1), lambda: Bag(-3, 40)):
try:
change()
except ValueError as e:
print("blocked:", e)
# blocked: kg must be >= 0: -1
# blocked: kg must be >= 0: -3
b.kg -= 30
# ValueError: kg must be >= 0: -10setattr로 넣어도, 새로 만들어도, -=로 줄여도 모두 막힙니다. b.kg -= 30은 b.kg를 읽고(__get__), 뺀 값을 다시 쓰는(__set__) 두 단계라 검색대를 피할 수 없습니다.
오 실장이 저울 기록을 다시 돌립니다. 음수 무게는 더 이상 올라오지 않습니다. 반장이 말합니다. "문을 막는 것보다, 모든 길을 검색대로 지나게 하는 게 낫지."
종결 보고 — LOST-010. 무게 검사가 __init__에만 있어 이후의 b.kg -= 30이 검사를 우회했음. 검사를 데이터 디스크립터(Positive)로 옮겨 모든 쓰기가 검색대를 지나게 해 해결.
Positive를 고쳐 하한값을 받게 하세요(kg = AtLeast(0), cm = AtLeast(10)). __init__에서 받은 값을 검사에 쓰면 됩니다.b.weigh = "x"를 더하면 b.weigh는 몇 단계에서 답이 나올까요? b.kg = "x"는요?__getattr__만 정의한 클래스에서, __init__에서 넣은 속성을 읽을 때 __getattr__이 불리는지 print로 확인하세요.반장의 업무 수첩
@property 로 속성 읽기·쓰기에 검사를 건다. 검사는 setter 한 곳에. setter가 없으면 읽기 전용.
디스크립터는 __get__/__set__ 을 가진 클래스 속성. __set_name__ 으로 붙은 이름을 알아 여러 속성에 재사용.
함수는 __get__ 만 가진 비데이터 디스크립터. b.m 을 읽을 때마다 새 바운드 메서드가 생긴다.
staticmethod 는 아무것도, classmethod 는 클래스를 묶는다.
읽기 순서: 데이터 디스크립터 > 인스턴스 __dict__ > 비데이터 디스크립터 > 클래스 속성 > __getattr__.
__getattribute__ 는 언제나, __getattr__ 은 못 찾았을 때만. 클래스의 클래스는 type.
새 교시·새 강의 소식 받기
이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.
R
ReflexKR
눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.
ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.
© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
질문과 답변
이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.