Descriptor 는 __get__, __set__, __delete__ 정의하는 모든 객체. 그런 객체가 클래스 속성으로 저장되면 인스턴스에서 접근 시 그 메서드들 통과 — 객체 직접 반환 X. @property 가 descriptor 로 구현. @classmethod, @staticmethod, 대부분 ORM 필드 타입도.
Data vs non-data descriptor
__set__ 가진 descriptor 는 *data descriptor*, 없는 건 *non-data descriptor*. 차이는 lookup 에 중요 — data descriptor 가 인스턴스 속성보다 이김, non-data 는 짐. property 가 data descriptor. 함수 (메서드 됨) 는 non-data — 그래서 self.method = something 으로 메서드 가릴 수 있어.
__set_name__ — 자기 이름 알기
Python 3.6+ 가 __set_name__(self, owner, name) 추가 — descriptor 가 클래스 속성에 할당될 때 호출. descriptor 한테 자기 이름 알려줘. 인스턴스별 state 관리 필요한 descriptor 에 유용 — 인스턴스에 관련 이름으로 저장 (descriptor x 면 _x 로).
__slots__ — 메모리 + 속성 제어
디폴트 — 모든 Python 인스턴스가 __dict__ 가져 임의 속성 추가 가능. 클래스에 __slots__ = ("x", "y") 가 Python 한테 — 이 클래스 인스턴스는 *오직* 이 속성, __dict__ 없음. 메모리 절약 큼 (인스턴스마다 dict 없음). 다른 속성 설정 시도하면 AttributeError.
__slots__ 가 값어치 할 때
수백만 인스턴스 만들 때. 10-100 객체엔 __dict__ 오버헤드 노이즈. 백만이면 절약 진짜. dataclass 버전엔 @dataclass(slots=True) (3.10+). 주의 — __slots__ 클래스는 임의 속성 나중에 추가 X — 정확히 그게 목적, 그게 비용.
원칙: Descriptor 와 __slots__ 는 필요할 때 손에 닿는 도구 — 디폴트 X. 대부분 애플리케이션 코드 안 만짐. 라이브러리 / 프레임워크 코드가 더 자주. 보면 인식해, 진짜 use case 일 때 손에 닿아.
Code
단순 descriptor — 검증·python
class Positive:
def __set_name__(self, owner, name):
self.attr = f"_{name}"
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.attr)
def __set__(self, instance, value):
if value <= 0:
raise ValueError("양수여야")
setattr(instance, self.attr, value)
class Account:
balance = Positive() # descriptor
def __init__(self, initial):
self.balance = initial # descriptor 의 __set__ 사용
a = Account(100)
print(a.balance) # 100 — __get__ 사용
a.balance = 200 # __set__ 사용
print(a.balance) # 200
try:
a.balance = -50 # __set__ 검증
except ValueError as e:
print(e)
@property 가 어떻게 구축됐나 — stdlib 의 descriptor·python
# @property 와 동일 — Python 의 실제 구현이 descriptor 사용
class MyProperty:
def __init__(self, fget):
self.fget = fget
def __get__(self, instance, owner):
if instance is None:
return self
return self.fget(instance)
class Circle:
def __init__(self, radius):
self.radius = radius
@MyProperty
def area(self):
return 3.14159 * self.radius ** 2
print(Circle(5).area) # 78.54 — @property 처럼 작동
__slots__ — 메모리 + 속성 잠금·python
class Point:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x = x
self.y = y
p = Point(3, 4)
print(p.x, p.y) # 3 4
# 새 속성 추가 X
try:
p.z = 5
except AttributeError as e:
print(e) # 'Point' object has no attribute 'z'
# 많은 인스턴스에 메모리 절약 큼
import sys
class Regular:
def __init__(self, x, y):
self.x, self.y = x, y
class Slotted:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x, self.y = x, y
print(sys.getsizeof(Regular(1, 2).__dict__) + sys.getsizeof(Regular(1, 2)))
# Slotted 인스턴스는 __dict__ 없음 — 상당히 작음
@dataclass(slots=True) — 현대 방법·python
from dataclasses import dataclass
@dataclass(slots=True) # 3.10+
class Point:
x: int
y: int
p = Point(3, 4)
print(p)
try:
p.z = 5
except AttributeError as e:
print(e)
# dataclass 의 모든 혜택 + __slots__ 의 메모리 + 잠금
TypedAttribute descriptor 구현 — 할당 시 타입 강제. class Thing: count = TypedAttribute(int); name = TypedAttribute(str). t.count = "five" 가 TypeError. __set_name__ 으로 descriptor 가 자기 이름 알고 인스턴스별 state 를 private 속성 이름 (_count, _name) 으로. 모든 path 테스트. 그 다음 클래스에 __slots__ 추가 — 뭐 변하는지 관찰 — __slots__ + descriptor 가 미묘하게 상호작용.
Progress
Progress is local-only — sign in to sync across devices.