본문으로 건너뛰기
Advertisement

__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__를 사용하면 메모리와 속도를 동시에 개선할 수 있습니다.

Advertisement