Tech

파이썬 (3)

grape.store 2025. 6. 30. 15:03
반응형

```

python dict 뒤에는 hash table engine이 존재한다.

```

- dict, set의 기반 코드는 여전히 hash table에 의존하지만, dict는 메모리 절약과 키 삽입 순서의 보존이라는 두가지 측면에서 최적화된다.(https://www.fluentpython.com/extra/internals-of-sets-and-dicts/)


### 성능

- 내부적으로 해시 테이블을 사용하여, 검색 시 전체를 스캔할 필요 없이 원하는 요소에 바로 접근할 수 있기 때문입니다. 반면, list는 항목을 찾기 위해 전체를 순차적으로 검색해야 하므로 항목 수가 많아질수록 성능이 크게 저하됩니다.

\### Hashes and Equality

\- 해시 테이블을 사용하려면 객체가 '해시 가능(hashable)'해야 합니다. 즉, \_\_hash\_\_()와 \_\_eq\_\_() 메서드를 모두 구현해야 합니다.

\- 두 객체가 같다면(a == b), 두 객체의 해시값도 반드시 같아야 합니다(hash(a) == hash(b)).

\- 서로 다른 객체가 같은 해시값을 갖는 것을 \*\*해시 충돌(hash collision)\*\*이라고 하며, Python은 이를 처리하는 메커니즘을 가지고 있습니다.

\### set

-   set의 핵심 데이터 구조는 '버킷(bucket)'이라 불리는 행들로 구성된 해시 테이블입니다.
-   요소를 추가할 때, 요소의 해시값을 계산하고 이를 기반으로 버킷의 위치(인덱스)를 결정합니다.
-   만약 해당 위치가 비어있으면 요소를 저장하고, 이미 다른 요소가 있다면(인덱스 충돌), 다음 비어있는 버킷을 찾아 저장합니다. 이러한 방식을 \*\*선형 탐사(linear probing)\*\*라고 합니다.
-   이러한 저장 방식 때문에 set의 요소들은 삽입된 순서대로 정렬되지 않습니다.

### dict
- 예전에는 indeces와 entries가 하나로 합쳐져있어 비 효율적인 메모리 구조를 가짐 size만큼 전부, key pointer, value pointer를 가져야 했음
- Compact dict부터는 indesces와 entries가 분리되어, indeesces는 size만큼 가져가지만, entries는 layz하게 가져갈수있음. 그리고 indesces와 entries가 분리되어 들어온순서대로 entries에 넣을수있으면서 넣은 순서를 보장할수있게됨. (indeces의 순서가 3이라도 entries의 순서는 0임, indecs가 entries 0 pointer를 가질거임)

키가 전부 문자열이고, 전체 인수에서 유일하게 식별할 수 있으면, 하나 이상의 인수에 딕셔너리 언패킹 **을 적용할 수 있다.
dump(**{'x':1}, y=2, **{'z':3})
-> {'x':1, 'y':2, 'z': 3}

내부에서 한번 더 사용하면 늦게 언패킹한애가 동일 key를 overwrite 한다.

Hasable?

  • 수명 주기 동안 절대 변하지 않는 hashcode가(hash를 통해 계산) 있고 다른 객체와 비교할수 있으면(eq) 객체를 해시 가능하다고 한다. 값이 같다고 판단되는 객체는 hashcode도 동일하다.
  • str, bytes는 모두 해시가능하며, container 형은 자신은 물론 포한된 모든 객체들도 불변할때만 hash가능하다. frozenset은 언제나 hash 가능하다.
  • tuple은 포함된 항목 모두가 해시가능해야만 해시가능하다.
  • 객체의 hashcode는 파이썬 버전, 하드웨어 아키텍처, 보안강화 마지막 추가된 salt로 다를수있다. 제대로 구현되었다면 하나의 python process안에서만 일정한 값이 나오는것이 보장된다.
  • 직접 정의한 class는 객체의 메모리주소를 이용하여 hash값으로 사용되기 때문에 해시가능합니다. 만일 eq를 오버라이딩 했다며, 반드시 hash도 오버라이딩 해야합니다.이때 "두 객체가 같다면, 해시값도 반드시 같아야 한다"는 규칙을 지켜야 합니다.

Pickle에 고려

  • 보안에 취약 (Insecure)
    • 신뢰할 수 없는 출처의 데이터를 unpickle 하는 것은 매우 위험합니다
  • 오래된 코드처럼 보임 (Old pickles look like old code)
    • 클래스 코드가 변경되면(예: 속성 추가), 나중에 이 데이터를 unpickle할 때 문제가 발생합니다.
  • 암시적 (Implicit)
    • 사용자가 형식을 지정할 수 없이 객체의 모든 구조를 그대로 직렬화합니다
  • 과도한 직렬화 (Over-serializes)
    • 객체에 포함된 모든 것을 직렬화합니다. 임시 캐시처럼 저장하고 싶지 않은 데이터까지 포함하며, 파일 객체처럼 직렬화할 수 없는 속성이 있으면 건너뛰는 대신 오류를 발생시킵니다.
  • init 미호출
    • unpickle 과정에서 객체가 다시 생성될 때, 생성자 메서드인 init가 호출되지 않습니다.
  • 파이썬 전용
    • Pickle은 Python에 특화된 포맷이라 다른 프로그래밍 언어에서는 거의 사용할 수 없습
  • 비가독성
    • Pickle 데이터는 바이너리 스트림이라 사람이 직접 파일을 열어 내용을 읽거나 디버깅하기가 어렵
  • 코드 직렬화처럼 보임
    • Pickle은 함수나 클래스의 코드를 저장하는 것이 아니라, 단순히 그 이름만 저장합니다. unpickle할 때는 현재 실행 중인 프로그램에서 그 이름을 찾아 코드를 가져옵니다. 코드를 저장하거나 옮기는 수단으로 착각해서는 안 됩니다.
    • 클래스가 삭제되었으므로 "그런 이름의 클래스를 찾을 수 없습니다"라는 AttributeError나 ModuleNotFoundError가 발생하며 실패
  • 느린 속도
    • JSON 등 다른 직렬화 방식에 비해 성능이 느릴 수 있습니다.

Pickle 대안

  • 대안으로는 JSON, marshmallow, cattrs, Protocol Buffers 등이 있으며,

dict 대신 UserDict를 상속받는 이유

  • dict를 직접 상속하면 위험하다: dict는 C언어로 구현되어 있어, setitem 같은 특별 메서드를 오버라이드(override)할 때 예기치 않은 무한 재귀 호출 등의 문제가 발생할 수 있습니다.
  • UserDict는 안전한 대안이다: UserDict는 dict를 상속받지 않고, 내부에 실제 dict 객체(self.data라는 속성)를 가지고 그것을 감싸는(wrapping) 클래스입니다. 따라서 메서드를 수정하거나 확장할 때 부작용 없이 훨씬 안전하고 간단합니다.

불변 매핑(Immutable Mapping)

  • 딕셔너리 데이터를 다른 곳에 전달하되 원본 데이터가 실수로 변경되는 것을 막고 싶을 때가 있습니다.
  • 예로 듭니다. GPIO 핀 정보가 담긴 딕셔너리(board.pins)가 있는데, 이 정보는 실제 하드웨어와 일치해야 하므로 소프트웨어에서 마음대로 바꿀 수 없어야 합니다.
  • MappingProxyType은 "데이터를 안전하게 공유하고 싶지만, 다른 사람이 수정하는 것은 막고 싶을 때" 사용하는 아주 유용한 도구입니다.

dict 설계 영향

  • 키(Key)는 '해시 가능(hashable)'해야 함, 객체는 반드시 hash()와 eq() 메서드를 제대로 구현하고 있어야 합니다.
  • 매우 빠른 검색 속도, dict는 내부적으로 해시 테이블을 사용합니다.
  • 순서 보장, 딕셔너리에 데이터를 넣은 순서가 그대로 유지됩니다. 이는 내부 메모리 구조가 '컴팩트 딕셔너리' 방식으로 변경
  • 메모리 오버헤드 존재, 빠른 속도를 유지하기 위해, 해시 테이블은 항상 일정 비율(약 1/3)의 빈 공간을 유지합니다. 이 때문에 약간의 메모리 오버헤드가 발생합니다.
  • 객체의 모든 속성은 가급적 init() 메서드 안에서 생성하는 것이 좋습니다.
    • 클래스의 인스턴스(객체) 속성은 내부적으로 dict라는 딕셔너리에 저장됩니다.
    • 같은 클래스의 모든 객체들이 init에서 동일한 속성들을 생성하면, Python은 이 객체들의 dict를 최적화합니다.
    • 즉, 속성 이름(키)들은 한 곳에만 저장하고, 각 객체는 값만 가지는 '키 공유' 방식으로 메모리를 절약합니다.
    • 만약 init 외부에서 새로운 속성을 추가하면(my_obj.new_attr = 1), 해당 객체는 더 이상 키를 공유하지 못하고 자신만의 비효율적인 dict를 새로 만들어야 합니다.

Set 설계 영향

  • 요소는 '해시 가능(hashable)'해야 함, 집합에 포함되는 모든 요소는 반드시 hash()와 eq() 메서드를 제대로 구현하고 있어야 합니다.
  • 매우 빠른 검색 속도, 특정 요소가 집합에 포함되어 있는지(in 연산자) 확인하는 속도가 매우 빠릅니다
  • 빠른 속도를 위해 해시 테이블을 유지해야 하므로, 리스트나 튜플 같은 단순 배열 구조보다 메모리를 더 많이 사용합니다
  • 순서가 보장되지 않음, 요소들이 저장되는 순서는 해시값과 삽입 순서에 따라 결정
  • 요소 추가 시 전체 순서 변경 가능
    • 집합에 새로운 요소를 추가하다가 내부 해시 테이블이 일정 수준(약 2/3) 이상 채워지면, 효율성 유지를 위해 파이썬이 더 큰 해시 테이블을 새로 만듭
    • 이 과정에서 기존의 모든 요소들을 새 테이블에 다시 삽입하기 때문에, 전체 요소의 순서가 완전히 뒤바뀔 수 있습니다.