__slots__ 메커니즘은 동적 속성 저장을 위한 인스턴스별 __dict__ 해시 테이블에 관련된 상당한 메모리 오버헤드를 해결하기 위해 Python 2.2에서 도입되었습니다. 문제는 수백 메가바이트의 RAM을 소모하는 수백만 개의 객체를 가진 고급 규모의 애플리케이션에서 발생합니다. 이는 메모리 압력과 캐시 미스를 발생시켜 성능을 저하시키는 문제를 발생시킵니다. 해결책은 클래스 변수로 __slots__를 선언하고 여기에 문자열 반복 가능 객체를 포함시키는 것으로, 이것은 인터프리터에게 해시 조회 대신에 속성을 위해 고정된 C 배열 오프셋을 예약하도록 지시합니다. 이렇게 하면 __dict__ 및 __weakref__ 슬롯이 명시적으로 요청되지 않는 한 제거됩니다.
이 최적화는 인스턴스별 메모리 사용량을 약 40-50% 줄이고 해싱 오버헤드를 피함으로써 속성 접근 속도를 가속화합니다. 또한 명시적으로 포함되지 않으면 __weakref__의 생성도 방지되어 객체 크기가 추가로 줄어듭니다. 그러나 이는 경직성을 도입합니다: 인스턴스는 동적으로 새로운 속성을 얻을 수 없고, 클래스 계층은 슬록 일관성을 유지해야 __dict__ 저장소로 조용히 되돌아가지 않도록 해야 합니다.
우리는 초당 천만 개의 네트워크 패킷을 처리하는 실시간 분석 파이프라인을 개발하는 동안 심각한 메모리 병목 현상에 직면했는데, 여기서 각 패킷은 표준 Python 객체로 표현되었습니다. 기본 __dict__ 기반 저장소는 객체 오버헤드로만 12GB의 RAM을 소모했습니다. 이로 인해 엄격한 10ms 대기 시간 SLA를 위반하게 되는 가비지 수집 중단이 발생했습니다.
솔루션 1: 사전 기반 레코드. 우리는 처음에 패킷 데이터를 일반 dict 인스턴스에 저장하는 것을 고려했습니다. 이는 단순성과 JSON 직렬화를 제공했지만, 프로파일링 결과 사전 해시 테이블이 여전히 객체당 48바이트의 오버헤드와 포인터 간접을 요구하여 메모리 사용량을 단 12% 줄이는 데 그쳤습니다. 메소드 캡슐화 부족으로 인해 비즈니스 로직이 유틸리티 모듈 간에 분산되었습니다.
솔루션 2: 명명된 튜플. collections.namedtuple로 전환하여 튜플의 C-구조 지원을 사용하여 인스턴스별 사전을 제거했습니다. 이로 인해 메모리가 크게 줄어들었지만, 불변성으로 인해 분석 중 패킷 타임스탬프를 업데이트할 수 없었고 기본값이나 검증 메소드를 추가할 수 없는 문제로 인해 어색한 어댑터 패턴을 강요받았습니다.
솔루션 3: __slots__ 클래스. 우리는 고정 속성 저장소를 사용하도록 Packet 클래스를 리팩토링했습니다:
class Packet: __slots__ = ('src_ip', 'dst_ip', 'payload', 'timestamp') def __init__(self, src_ip, dst_ip, payload, timestamp): self.src_ip = src_ip self.dst_ip = dst_ip self.payload = payload self.timestamp = timestamp def size(self): return len(self.payload)
이것은 우리의 객체 지향 디자인을 유지하면서 __dict__를 완전히 제거했습니다. 우리는 메모리 효율성과 코드 유지보수성을 균형 있게 유지했기 때문에 이 접근 방식을 선택했으며, 객체 풀의 약한 참조 캐시를 지원하기 위해 '__weakref__'를 명시적으로 포함시켜야 했습니다.
결과. 메모리 사용량이 4.5GB로 감소하여 파이프라인이 일반 하드웨어에서 실행될 수 있게 되었습니다. 해시 테이블 프로브 대신 직접 오프셋 계산 덕분에 속성 접근 속도가 35% 빨라졌지만, 동적 속성 주입에 대해 __dict__에 의존하던 디버깅 코드를 리팩토링해야 했습니다.
부모 클래스가 충돌하는 슬롯 레이아웃을 정의할 때 __slots__는 다중 상속과 어떻게 상호작용합니까?
하위 클래스가 __slots__를 사용하여 여러 부모로부터 상속받을 때, Python은 결합된 슬롯 레이아웃이 겹치지 않는 이름 없이 일관된 선형 순서를 형성해야 한다고 요구합니다. 부모가 슬롯에 속성 이름을 공유하거나 한 부모가 __slots__를 사용하는 동안 다른 부모가 기본 __dict__를 사용하는 경우, 인터프리터는 하위 클래스에 대해 어쨌든 __dict__를 생성하여 메모리 절약 효과를 조용히 무효화합니다. 이는 Python이 부모 슬롯을 연결하여 단일 슬롯 테이블을 구성하기 때문입니다. 후보자는 모든 부모가 이상적으로 __slots__를 사용해야 하며, 하위 클래스는 딕셔너리로의 fallback을 피하기 위해 추가 슬롯을 명시적으로 선언해야 한다는 점을 이해해야 합니다.
왜 표준 pickle 모듈이 사용자 정의 상태 메소드 없이 슬롯 객체를 복원하는 데 실패합니까?
기본적으로 pickle은 객체의 상태를 __dict__ 속성을 통해 저장하고 복원하려고 시도합니다. 슬롯 클래스는 명시적으로 추가되지 않는 한 이 사전을 포함하지 않으므로, 언피클링 중 로더가 존재하지 않는 슬롯에 할당하려고 시도할 때 AttributeError를 발생시킵니다. 이 문제를 해결하려면 슬롯 값을 포함하는 사전을 반환하기 위해 __getstate__를 구현하고, 이를 복원하기 위해 __setstate__를 구현하거나 __reduce_ex__ 프로토콜을 사용해야 합니다. 많은 후보자들이 __slots__가 객체 레이아웃 계약을 변경한다는 점을 간과하며, pickle이 슬롯 설명자에 대해 자동으로 반사를 사용한다고 가정합니다.
__slots__가 런타임에 인스턴스 속성을 동적으로 추가하는 것을 방지합니까?
네, 그러나 부모 클래스가 __dict__를 제공하지 않고 '__dict__'가 슬롯 목록에 명시적으로 포함되지 않은 경우에만 그렇습니다. 후보자들은 종종 __slots__가 단순히 __dict__ 속성을 제거한다고 생각하고, 기본 클래스가 기본 사전 저장소를 유지하는 경우에는 인스턴스가 여전히 그 상속받은 사전을 통해 임의 속성을 수용할 수 있다는 사실을 간과합니다. 또한, 슬롯 인스턴스는 기존 속성과 관련하여 여전히 가변적이며, 클래스 수준에서 여전히 몽키 패치가 가능합니다. 진정한 불변성을 요구하려면 __setattr__을 재정의하는 것과 같은 추가 조치가 필요합니다. 단순히 __slots__를 사용하는 것만으로는 충분하지 않습니다.