__slots__ — 메모리 최적화와 속성 제한
__slots__는 클래스의 인스턴스가 가질 수 있는 속성을 미리 선언하여 메모리를 절약하고 속성 접근 속도를 높이는 기능입니다.
__slots__ 기본
import sys
# 기본 클래스 — __dict__ 사용
class PointWithDict:
def __init__(self, x: float, y: float):
self.x = x
self.y = y
# __slots__ 사용 클래스
class PointWithSlots:
__slots__ = ("x", "y")
def __init__(self, x: float, y: float):
self.x = x
self.y = y
p_dict = PointWithDict(1.0, 2.0)
p_slots = PointWithSlots(1.0, 2.0)
# 메모리 비교
print(f"__dict__ 버전: {sys.getsizeof(p_dict) + sys.getsizeof(p_dict.__dict__)} bytes")
print(f"__slots__ 버전: {sys.getsizeof(p_slots)} bytes")
# __slots__ 인스턴스는 __dict__가 없음
print(hasattr(p_dict, "__dict__")) # True
print(hasattr(p_slots, "__dict__")) # False
# 허용되지 않은 속성 할당 시도
try:
p_slots.z = 3.0 # AttributeError!
except AttributeError as e:
print(f"오류: {e}")
대량 인스턴스에서 메모리 절약 효과
import tracemalloc
import sys
class LogEntryDict:
def __init__(self, timestamp: str, level: str, message: str):
self.timestamp = timestamp
self.level = level
self.message = message
class LogEntrySlots:
__slots__ = ("timestamp", "level", "message")
def __init__(self, timestamp: str, level: str, message: str):
self.timestamp = timestamp
self.level = level
self.message = message
N = 100_000
# __dict__ 방식
tracemalloc.start()
dict_entries = [
LogEntryDict("2024-01-15 10:00:00", "INFO", f"메시지 {i}")
for i in range(N)
]
_, peak_dict = tracemalloc.get_traced_memory()
tracemalloc.stop()
del dict_entries
# __slots__ 방식
tracemalloc.start()
slots_entries = [
LogEntrySlots("2024-01-15 10:00:00", "INFO", f"메시지 {i}")
for i in range(N)
]
_, peak_slots = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"__dict__ 방식: {peak_dict / 1024 / 1024:.1f} MB")
print(f"__slots__ 방식: {peak_slots / 1024 / 1024:.1f} MB")
print(f"절약: {(1 - peak_slots / peak_dict) * 100:.1f}%")
상속과 __slots__
# 부모 클래스에 __slots__ 사용 시 자식 클래스 주의사항
class Base:
__slots__ = ("x", "y")
def __init__(self, x: float, y: float):
self.x = x
self.y = y
class ChildWithSlots(Base):
"""__slots__ 추가 정의 — 완전한 최적화"""
__slots__ = ("z",)
def __init__(self, x: float, y: float, z: float):
super().__init__(x, y)
self.z = z
class ChildWithoutSlots(Base):
"""__slots__ 미정의 — __dict__ 생성됨 (절약 효과 감소)"""
def __init__(self, x: float, y: float, name: str):
super().__init__(x, y)
self.name = name # 이 속성은 __dict__에 저장
c1 = ChildWithSlots(1, 2, 3)
c2 = ChildWithoutSlots(1, 2, "test")
print(f"ChildWithSlots __slots__: {ChildWithSlots.__slots__}")
print(f"ChildWithoutSlots has __dict__: {hasattr(c2, '__dict__')}")
# 전체 슬롯 확인 (부모 포함)
def get_all_slots(cls) -> list[str]:
slots = []
for klass in cls.__mro__:
slots.extend(getattr(klass, "__slots__", []))
return slots
print(f"ChildWithSlots 전체 슬롯: {get_all_slots(ChildWithSlots)}")
dataclass와 __slots__
from dataclasses import dataclass
# Python 3.10+: dataclass에 slots=True 옵션
@dataclass(slots=True)
class Pixel:
x: int
y: int
r: int = 0
g: int = 0
b: int = 0
a: int = 255
def to_tuple(self) -> tuple:
return (self.x, self.y, self.r, self.g, self.b, self.a)
# Python 3.9 이하에서는 수동으로 __slots__ 정의
@dataclass
class PixelManual:
__slots__ = ("x", "y", "r", "g", "b", "a")
x: int
y: int
r: int
g: int
b: int
a: int
import sys
p1 = Pixel(100, 200, 255, 128, 0)
print(p1)
print(f"슬롯 사용: {not hasattr(p1, '__dict__')}")
# 이미지 처리 시 메모리 절약 효과
import tracemalloc
WIDTH, HEIGHT = 100, 100 # 10,000 픽셀
tracemalloc.start()
pixels = [Pixel(x, y, x % 256, y % 256, 0) for y in range(HEIGHT) for x in range(WIDTH)]
_, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"10,000 픽셀 (slots): {peak / 1024:.1f} KB")
__slots__와 __weakref__
import weakref
class RegularNode:
def __init__(self, value):
self.value = value
class SlottedNode:
__slots__ = ("value",) # weakref 지원 안 됨!
def __init__(self, value):
self.value = value
class SlottedNodeWithWeakref:
__slots__ = ("value", "__weakref__") # weakref 명시적 포함
def __init__(self, value):
self.value = value
# weakref 테스트
regular = RegularNode(42)
ref_regular = weakref.ref(regular)
print(f"일반 노드 weakref: {ref_regular()}") # OK
slotted = SlottedNode(42)
try:
ref_slotted = weakref.ref(slotted)
except TypeError as e:
print(f"슬롯 노드 weakref 오류: {e}")
slotted_wr = SlottedNodeWithWeakref(42)
ref_wr = weakref.ref(slotted_wr)
print(f"__weakref__ 포함 슬롯 노드: {ref_wr()}") # OK
실전 예제: 대용량 데이터 처리 클래스
from __future__ import annotations
import csv
import sys
from pathlib import Path
class Transaction:
"""금융 트랜잭션 레코드 — 수백만 개 처리를 위해 __slots__ 최적화"""
__slots__ = (
"transaction_id",
"timestamp",
"amount",
"currency",
"merchant",
"category",
"status",
)
def __init__(
self,
transaction_id: str,
timestamp: str,
amount: float,
currency: str,
merchant: str,
category: str,
status: str = "completed",
):
self.transaction_id = transaction_id
self.timestamp = timestamp
self.amount = amount
self.currency = currency
self.merchant = merchant
self.category = category
self.status = status
def __repr__(self) -> str:
return (f"Transaction({self.transaction_id!r}, "
f"{self.amount} {self.currency})")
@property
def is_large(self) -> bool:
return self.amount > 1_000_000
def to_dict(self) -> dict:
return {slot: getattr(self, slot) for slot in self.__slots__}
def load_transactions(filepath: str, limit: int | None = None) -> list[Transaction]:
"""CSV에서 트랜잭션 로드 — __slots__로 메모리 절약"""
transactions = []
with open(filepath, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for i, row in enumerate(reader):
if limit and i >= limit:
break
transactions.append(Transaction(
transaction_id=row["id"],
timestamp=row["timestamp"],
amount=float(row["amount"]),
currency=row.get("currency", "KRW"),
merchant=row["merchant"],
category=row.get("category", "기타"),
status=row.get("status", "completed"),
))
return transactions
def analyze_transactions(transactions: list[Transaction]) -> dict:
"""트랜잭션 집계 분석"""
total = sum(t.amount for t in transactions)
by_category: dict[str, float] = {}
large_count = 0
for t in transactions:
by_category[t.category] = by_category.get(t.category, 0) + t.amount
if t.is_large:
large_count += 1
return {
"count": len(transactions),
"total_amount": total,
"average_amount": total / len(transactions) if transactions else 0,
"large_transactions": large_count,
"by_category": dict(sorted(by_category.items(), key=lambda x: -x[1])),
}
# 메모리 사용량 비교 시뮬레이션
import tracemalloc
import random
def make_sample_transactions(n: int) -> list[Transaction]:
categories = ["식비", "교통", "쇼핑", "의료", "교육"]
merchants = ["스타벅스", "이마트", "현대백화점", "병원", "학원"]
result = []
for i in range(n):
result.append(Transaction(
transaction_id=f"TXN{i:08d}",
timestamp=f"2024-01-{(i % 28) + 1:02d}",
amount=round(random.uniform(1000, 500000), 2),
currency="KRW",
merchant=random.choice(merchants),
category=random.choice(categories),
))
return result
tracemalloc.start()
txns = make_sample_transactions(50_000)
_, peak = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"50,000 트랜잭션 메모리: {peak / 1024 / 1024:.1f} MB")
print(f"트랜잭션당: {peak / len(txns):.0f} bytes")
report = analyze_transactions(txns)
print(f"총 금액: {report['total_amount']:,.0f}원")
print(f"평균 금액: {report['average_amount']:,.0f}원")
고수 팁
1. __slots__와 __dict__ 혼용
class FlexibleSlots:
"""일부 속성은 슬롯으로, 나머지는 __dict__로"""
__slots__ = ("x", "y", "__dict__") # __dict__ 포함 → 추가 속성 허용
def __init__(self, x: float, y: float):
self.x = x
self.y = y
p = FlexibleSlots(1.0, 2.0)
p.extra = "추가 속성" # __dict__가 있으므로 가능
print(p.x, p.extra)
2. 프로퍼티와 __slots__ 조합
class Temperature:
__slots__ = ("_celsius",)
def __init__(self, celsius: float):
self._celsius = celsius
@property
def celsius(self) -> float:
return self._celsius
@celsius.setter
def celsius(self, value: float) -> None:
if value < -273.15:
raise ValueError(f"절대 영도 이하: {value}")
self._celsius = value
@property
def fahrenheit(self) -> float:
return self._celsius * 9/5 + 32
@property
def kelvin(self) -> float:
return self._celsius + 273.15
t = Temperature(100)
print(f"섭씨: {t.celsius}°C")
print(f"화씨: {t.fahrenheit}°F")
print(f"켈빈: {t.kelvin}K")
t.celsius = -10
print(f"변경 후: {t.celsius}°C")
정리
| 항목 | __dict__ | __slots__ |
|---|---|---|
| 메모리 사용 | 많음 (딕셔너리 오버헤드) | 적음 (40~50% 절약) |
| 속성 접근 속도 | 해시 조회 | 오프셋 직접 접근 (빠름) |
| 동적 속성 추가 | 가능 | 불가 (기본) |
| weakref | 가능 | __weakref__ 명시 필요 |
| pickle | 기본 지원 | __getstate__/__setstate__ 필요할 수 있음 |
| 사용 시기 | 범용 | 수만 개 이상의 인스턴스, 성능 중요 시 |
수십만 개 이상의 인스턴스를 생성하는 경우 __slots__를 사용하면 메모리와 속도를 동시에 개선할 수 있습니다.