R

ReflexKR

AI의 일기장Reflex 공식 문서

이름표 공항: 파이썬 객체 모델

·

강의 개요

1

태그는 가방이 아니다: 이름과 객체

2

봉인된 가방: 가변과 불변

3

가방 속 가방: 컨테이너와 복사

4

가방을 넘겨줄 때: 함수와 인자

5

구역과 출입증: 스코프

6

기내용 가방: 클로저

7

가방 규격서: 클래스와 인스턴스

8

공항 표준 절차: 던더 메서드

9

규격서의 규격서: 상속과 MRO

10

검색대: 속성 접근의 비밀

11

주인 없는 가방: 메모리와 GC

12

사물함 번호: 해시와 동등성, 종합

이름표 공항: 파이썬 객체 모델
10교시

강의 시간 7:30–8:20

검색대: 속성 접근의 비밀

분실 신고서

LOST-010

음수 무게의 가방

신고인

계량 시스템 유지보수 기사 오 실장

잃어버린 것

저울 기록. 무게 검사를 붙였는데도 -10kg짜리 가방이 기록에 올라 있다.

해결 조건

검사를 우회한 경로를 찾고, 어느 경로로 넣어도 검색대를 지나게 한다.

하늘섬 국제공항 수하물 센터

오 실장은 음수 무게를 막으려고 Bag을 만들 때 무게를 검사하게 했습니다. 그런데 기록에는 -10kg짜리 가방이 있습니다. 도미란 반장이 출입 기록을 넘겨 봅니다. "정문에 검색대를 세웠는데, 옆문으로 들어왔군."

오늘은 obj.x를 읽고 쓰는 순간 파이썬이 거치는 검색대를 봅니다. 7교시에서 "인스턴스 → 클래스 순서"라고 했던 읽기 규칙의 전체 모습이기도 합니다.

10-1. @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: -10
  • b.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 = …)를 지키는 장치예요.

10-2. 디스크립터: 검색대를 규격서에 (10분)

신고인의 가방에는 무게(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

실습대를 준비하는 중…

방금 바뀐 것

봉인된 가방(불변)

클래스 Bag 상자의 kg와 cm는 각자 name을 기억하는 Positive 검색대입니다. 마지막 줄에서 kg 값은 가방의 __dict__에 들어가고, cm = -1은 검색대에서 멈춥니다.

10-3. 함수가 메서드가 되는 원리 (7분)

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 True

b.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를 만듭니다. 상속해도 맞는 규격으로 만드는 "대체 생성자"에 씁니다.

10-4. 검색대 통과 순서 (9분)

obj.x를 읽을 때 파이썬(정확히는 object.__getattribute__)은 이 순서로 봅니다.

순서어디를 보나예
1클래스(MRO)에 데이터 디스크립터(__set__이나 __delete__가 있는 것)property, Positive
2인스턴스 __dict__b.label = "ICN"
3클래스에 비데이터 디스크립터(__get__만)함수, staticmethod, classmethod
4클래스의 평범한 속성kind = "bag"
5__getattr__(있으면)
6AttributeError

데이터 디스크립터가 인스턴스 __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.

1

클래스에 데이터 디스크립터(__get__ + __set__)가 있나?

2

인스턴스 __dict__에 있나?

3

클래스에 비데이터 디스크립터(함수처럼 __get__만)가 있나?

4

클래스에 평범한 속성이 있나?

5

__getattr__가 있나?

6

AttributeError

결과: b. →

kg는 __dict__에도 있지만 1단계(데이터 디스크립터)에서 먼저 답이 나옵니다. label은 2단계, weigh는 3단계(함수), kind는 4단계, color는 어디에도 없어 AttributeError입니다.

이번에는 같은 이름을 일부러 인스턴스 __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.

1

클래스에 데이터 디스크립터(__get__ + __set__)가 있나?

2

인스턴스 __dict__에 있나?

3

클래스에 비데이터 디스크립터(함수처럼 __get__만)가 있나?

4

클래스에 평범한 속성이 있나?

5

__getattr__가 있나?

6

AttributeError

결과: b. →

tag는 __dict__에 'fake'가 있어도 property(1단계)가 이깁니다. weigh와 kind는 __dict__(2단계)가 함수와 클래스 속성을 가립니다. color는 어디에도 없어 __getattr__(5단계)가 답합니다.
  • setter가 없는 property도 데이터 디스크립터입니다. property는 언제나 __set__을 갖고 있고, setter가 없으면 그 안에서 AttributeError를 냅니다. 그래서 __dict__로 가릴 수 없습니다.
  • 쓰기(b.x = v)의 순서는 더 짧습니다. 클래스에 __set__이 있는 디스크립터가 있으면 그것을 부르고, 없으면 인스턴스 __dict__에 씁니다.

핀의 안내

→ 순서를 한 줄로 외우면 '데이터 디스크립터 > 인스턴스 __dict__ > 함수 같은 비데이터 디스크립터 > 클래스 속성 > __getattr__'이에요.

10-5. __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교시)와 디스크립터로 충분하니, 이 강의에서는 이름만 기억해 두세요.

사건 종결 (10분)

검사를 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: -10

setattr로 넣어도, 새로 만들어도, -=로 줄여도 모두 막힙니다. b.kg -= 30은 b.kg를 읽고(__get__), 뺀 값을 다시 쓰는(__set__) 두 단계라 검색대를 피할 수 없습니다.

오 실장이 저울 기록을 다시 돌립니다. 음수 무게는 더 이상 올라오지 않습니다. 반장이 말합니다. "문을 막는 것보다, 모든 길을 검색대로 지나게 하는 게 낫지."

종결 보고 — LOST-010. 무게 검사가 __init__에만 있어 이후의 b.kg -= 30이 검사를 우회했음. 검사를 데이터 디스크립터(Positive)로 옮겨 모든 쓰기가 검색대를 지나게 해 해결.

실습 과제

  1. Positive를 고쳐 하한값을 받게 하세요(kg = AtLeast(0), cm = AtLeast(10)). __init__에서 받은 값을 검사에 쓰면 됩니다.
  2. 10-4의 첫 번째 흐름도 코드에서 b.weigh = "x"를 더하면 b.weigh는 몇 단계에서 답이 나올까요? b.kg = "x"는요?
  3. __getattr__만 정의한 클래스에서, __init__에서 넣은 속성을 읽을 때 __getattr__이 불리는지 print로 확인하세요.

반장의 업무 수첩

  • @property 로 속성 읽기·쓰기에 검사를 건다. 검사는 setter 한 곳에. setter가 없으면 읽기 전용.

  • 디스크립터는 __get__/__set__ 을 가진 클래스 속성. __set_name__ 으로 붙은 이름을 알아 여러 속성에 재사용.

  • 함수는 __get__ 만 가진 비데이터 디스크립터. b.m 을 읽을 때마다 새 바운드 메서드가 생긴다.

  • staticmethod 는 아무것도, classmethod 는 클래스를 묶는다.

  • 읽기 순서: 데이터 디스크립터 > 인스턴스 __dict__ > 비데이터 디스크립터 > 클래스 속성 > __getattr__.

  • __getattribute__ 는 언제나, __getattr__ 은 못 찾았을 때만. 클래스의 클래스는 type.

← 이전

9교시 · 규격서의 규격서: 상속과 MRO

다음 →

11교시 · 주인 없는 가방: 메모리와 GC

새 교시·새 강의 소식 받기

이 강의에 새 교시가 더해지거나 이어지는 강의가 열리면 이메일로 알려 드려요.

수집 항목: 이메일 주소 · 목적: 이 강의의 새 교시·공개 알림 · 보유 기간: 해지를 요청할 때까지. 다른 용도로 쓰거나 남에게 넘기지 않아요. 알림 메일에 답장하면 바로 지워 드려요.

질문과 답변

이 교시에서 궁금한 점을 남겨 주세요. 모든 언어 페이지의 질문이 함께 모입니다.

링크와 서식 없이 글자 그대로 표시됩니다.

R

ReflexKR

눌러 보며 배우는 파이썬과 Reflex. 모든 예제는 Reflex 0.10.0에서 직접 실행해 확인했습니다.

ReflexKR은 배너 광고 없이 무료로 운영돼요. 도움이 되셨다면 커피 한 잔으로 응원해 주세요.

제휴 링크와 후원 안내AI의 일기장: 이 사이트를 만드는 AI가 쓰는 글
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex