Python dict에 동일한 해시를 가진 여러 키가있는 이유는 무엇입니까?


90

hash후드 아래에서 Python 함수 를 이해하려고합니다 . 모든 인스턴스가 동일한 해시 값을 반환하는 사용자 지정 클래스를 만들었습니다.

class C:
    def __hash__(self):
        return 42

위의 클래스의 인스턴스는 한 번에 하나만있을 수 있다고 가정 dict했지만 실제로 dict는 동일한 해시를 가진 여러 요소를 가질 수 있습니다.

c, d = C(), C()
x = {c: 'c', d: 'd'}
print(x)
# {<__main__.C object at 0x7f0824087b80>: 'c', <__main__.C object at 0x7f0823ae2d60>: 'd'}
# note that the dict has 2 elements

좀 더 실험을 해보니 __eq__클래스의 모든 인스턴스가 동일하게 비교 되도록 메서드를 재정의 dict하면 하나의 인스턴스 만 허용 된다는 것을 알았습니다 .

class D:
    def __hash__(self):
        return 42
    def __eq__(self, other):
        return True

p, q = D(), D()
y = {p: 'p', q: 'q'}
print(y)
# {<__main__.D object at 0x7f0823a9af40>: 'q'}
# note that the dict only has 1 element

따라서 dict동일한 해시를 가진 여러 요소를 가질 수있는 방법을 알고 싶습니다 .


3
스스로 발견했듯이 세트와 딕셔너리는 객체가 같지 않은 경우 동일한 해시를 가진 여러 객체를 포함 할 수 있습니다. 뭘 물어 보는 거냐? 테이블은 어떻게 작동합니까? 많은 기존 자료가있는 매우 일반적인 질문입니다 ...

@delnan 질문을 게시 한 후 이것에 대해 더 많이 생각하고있었습니다. 이 동작은 Python으로 제한 될 수 없습니다. 그리고 당신이 옳습니다. 일반적인 해시 테이블 문헌에 대해 더 깊이 탐구해야한다고 생각합니다. 감사.
— Praveen Gollakota

답변:


55

Python의 해싱이 어떻게 작동하는지에 대한 자세한 설명은 왜 조기 반환이 다른 것보다 느린가요?

기본적으로 해시를 사용하여 테이블의 슬롯을 선택합니다. 슬롯에 값이 있고 해시가 일치하면 항목을 비교하여 동일한 지 확인합니다.

해시가 일치하지 않거나 항목이 같지 않으면 다른 슬롯을 시도합니다. 이것을 선택하는 공식이 있습니다 (참조 된 답변에서 설명합니다), 해시 값의 사용되지 않는 부분을 점차적으로 가져옵니다. 그러나 일단 모두 사용하면 결국 해시 테이블의 모든 슬롯을 통해 작동합니다. 결국 일치하는 항목이나 빈 슬롯을 찾게됩니다. 검색에서 빈 슬롯을 찾으면 값을 삽입하거나 포기합니다 (값을 추가하거나 가져 오는지 여부에 따라 다름).

주목해야 할 중요한 점은 목록이나 버킷이 없다는 것입니다. 특정 수의 슬롯이있는 해시 테이블 만 있고 각 해시는 후보 슬롯의 시퀀스를 생성하는 데 사용됩니다.


7
해시 테이블 구현에 대한 올바른 방향을 알려 주셔서 감사합니다. 해시 테이블에 대해 원했던 것보다 훨씬 더 많이 읽었으며 별도의 답변으로 결과를 설명했습니다. stackoverflow.com/a/9022664/553995
— Praveen Gollakota 2012 년

112

여기에 내가 모을 수 있었던 파이썬 딕셔너리에 대한 모든 것이 있습니다 (아마 누구도 알고 싶어하는 것보다 더 많을 것입니다.하지만 대답은 포괄적입니다). Python dicts가 슬롯을 사용하고 나를이 토끼 굴로 이끄는 것을 지적한 Duncan 에게 외침 .

  • 파이썬 사전은 해시 테이블 로 구현됩니다 .
  • 해시 테이블은 해시 충돌을 허용해야합니다. 즉, 두 개의 키가 동일한 해시 값을 가지고 있더라도 테이블 구현에는 키와 값 쌍을 모호하지 않게 삽입하고 검색하는 전략이 있어야합니다.
  • Python dict는 개방 주소 지정 을 사용 하여 해시 충돌을 해결합니다 (아래 설명 됨) ( dictobject.c : 296-297 참조 ).
  • 파이썬 해시 테이블은 연속적인 메모리 블록 일뿐입니다 (배열과 비슷하므로 O(1)인덱스로 조회 할 수 있습니다 ).
  • 테이블의 각 슬롯은 하나의 항목 만 저장할 수 있습니다. 이것은 중요하다
  • 테이블의 각 항목 은 실제로 세 값의 조합입니다.. 이것은 C 구조체로 구현됩니다 ( dictobject.h : 51-56 참조 ).
  • 아래 그림은 파이썬 해시 테이블의 논리적 표현입니다. 아래 그림에서 왼쪽의 0, 1, ..., i, ... 는 해시 테이블 에있는 슬롯의 인덱스입니다 (이들은 단지 설명을위한 것이며 테이블과 함께 저장되지 않습니다!).

    # Logical model of Python Hash table
    -+-----------------+
    0| <hash|key|value>|
    -+-----------------+
    1|      ...        |
    -+-----------------+
    .|      ...        |
    -+-----------------+
    i|      ...        |
    -+-----------------+
    .|      ...        |
    -+-----------------+
    n|      ...        |
    -+-----------------+
    
  • 새로운 dict가 초기화되면 8 개의 슬롯으로 시작 합니다 . ( dictobject.h : 49 참조 )

  • 테이블에 항목을 추가 할 때 i키의 해시를 기반으로하는 슬롯으로 시작 합니다. CPython은 초기 i = hash(key) & mask. 어디 mask = PyDictMINSIZE - 1,하지만 그다지 중요하지 않습니다). 확인되는 초기 슬롯 i 는 키 의 해시 에 따라 다릅니다 .
  • 해당 슬롯이 비어 있으면 항목이 슬롯에 추가됩니다 (항목에 따라 <hash|key|value>). 그러나 그 슬롯이 점유되면 어떨까요!? 다른 항목에 동일한 해시 (해시 충돌!)가 있기 때문일 가능성이 높습니다.
  • 슬롯이 점유 된 경우 CPython (및 PyPy)은 슬롯에있는 항목 의 해시와 키 ( ==비교가 아닌 is비교를 통해)를 삽입 할 현재 항목의 키 ( dictobject.c : 337 , 344-345 ). 경우 모두 일치, 다음은 항목이 이미 생각하고 포기하고 다음 항목에 이동 삽입 할 수 있습니다. 중 해시 또는 키가 일치하지 않는 경우, 시작 프로빙 .
  • Probing은 빈 슬롯을 찾기 위해 슬롯별로 슬롯을 검색한다는 의미입니다. 기술적으로 우리는 하나씩, i + 1, i + 2, ...로 이동하고 첫 번째 사용 가능한 것을 사용할 수 있습니다 (선형 프로빙). 그러나 주석 ( dictobject.c : 33-126 참조)에서 아름답게 설명 된 이유 때문에 CPython은 임의 조사를 사용합니다 . 랜덤 프로빙에서 다음 슬롯은 의사 랜덤 순서로 선택됩니다. 항목이 첫 번째 빈 슬롯에 추가됩니다. 이 논의에서 다음 슬롯을 선택하는 데 사용되는 실제 알고리즘은 실제로 중요하지 않습니다 ( 프로빙 알고리즘 은 dictobject.c : 33-126 참조 ). 중요한 것은 첫 번째 빈 슬롯을 찾을 때까지 슬롯이 검색된다는 것입니다.
  • 조회에서도 똑같은 일이 발생합니다. 초기 슬롯 i (여기서 i는 키의 해시에 따라 다름)로 시작합니다. 해시와 키가 모두 슬롯의 항목과 일치하지 않으면 일치하는 슬롯을 찾을 때까지 검색을 시작합니다. 모든 슬롯이 소진되면 실패를보고합니다.
  • BTW, 딕셔너리는 2/3가 차면 크기가 조정됩니다. 이렇게하면 조회 속도가 느려지지 않습니다. ( dictobject.h : 64-65 참조 )

됐어요! dict의 Python 구현은 ==항목을 삽입 할 때 두 키의 해시 같음과 키의 정상 같음 ( ) 을 모두 확인 합니다. 요약하면, 두 개의 키 a와 b및 hash(a)==hash(b),하지만 a!=b이면 둘 다 파이썬 딕셔너리에 조화롭게 존재할 수 있습니다. 그러나 만약 hash(a)==hash(b) 및 a==b 이면 둘 다 같은 사전에있을 수 없습니다.

모든 해시 충돌 후 조사해야하기 때문에 너무 많은 해시 충돌의 한 가지 부작용은 조회 및 삽입이 매우 느려진다는 것입니다 (Duncan이 주석 에서 지적했듯이 ).

내 질문에 대한 짧은 대답은 "그게 소스 코드에서 구현되는 방식이기 때문에"입니다.

이것은 알아두면 좋지만 (괴짜 포인트에 대해?) 실생활에서 어떻게 사용할 수 있는지 잘 모르겠습니다. 왜냐하면 당신이 명시 적으로 무언가를 깨뜨 리려고하지 않는 한, 같지 않은 두 객체가 같은 해시를 가지는 이유는 무엇입니까?


8
이것은 사전을 채우는 방법을 설명합니다. 그러나 key_value 쌍을 검색하는 동안 해시 충돌이 발생하면 어떻게 될까요? 2 개의 객체 A와 B가 있는데, 둘 다 4로 해시됩니다. 따라서 먼저 A에 슬롯 4가 할당 된 다음 B에 무작위 프로빙을 통해 슬롯이 할당됩니다. B. B 해시를 4로 검색하고 싶을 때 python은 먼저 슬롯 4를 확인하지만 키가 일치하지 않아 A를 반환 할 수 없습니다. B의 슬롯이 랜덤 프로빙에 의해 할당 되었기 때문에 B가 어떻게 다시 반환되는지 O (1) 시간에?
— sayantankhan 2014

4
@ Bolt64 무작위 프로빙은 실제로 무작위가 아닙니다. 동일한 키 값에 대해 항상 동일한 프로브 시퀀스를 따르므로 결국 B를 찾습니다. 충돌이 많으면 더 오래 걸릴 수 있으므로 사전이 O (1)임을 보장 할 수는 없습니다. 이전 버전의 Python에서는 충돌하는 일련의 키를 쉽게 구성 할 수 있으며이 경우 사전 조회가 O (n)이됩니다. 이것은 DoS 공격에 대한 가능한 벡터이므로 최신 Python 버전은 해싱을 수정하여 의도적으로 수행하기 어렵게 만듭니다.
— Duncan

2
@Duncan A가 삭제되고 B에 대한 조회를 수행하면 어떻게 될까요? 실제로 항목을 삭제하지 않고 삭제 된 것으로 표시 하시겠습니까? 즉, dicts 연속 삽입과 삭제에 적합하지 않은 것을 의미 ....
— 세대는-YS

2
@ gen-ys yes 삭제 및 미사용은 조회를 위해 다르게 처리됩니다. 사용하지 않음은 일치하는 검색을 중지하지만 삭제는 그렇지 않습니다. 삽입시 삭제되거나 사용되지 않는 것은 사용할 수있는 빈 슬롯으로 처리됩니다. 연속 삽입 및 삭제는 괜찮습니다. 사용되지 않은 (삭제되지 않은) 슬롯의 수가 너무 낮아지면 해시 테이블이 현재 테이블에 비해 너무 커진 것과 같은 방식으로 다시 빌드됩니다.
— 던컨

1
이것은 Duncan이 해결하려고 시도한 충돌 지점에 대한 좋은 대답이 아닙니다. 귀하의 질문에서 구현에 대한 참조에 특히 좋지 않은 답변입니다. 이것을 이해하는 데 가장 중요한 것은 충돌이 발생하면 파이썬이 해시 테이블의 다음 오프셋을 계산하기 위해 공식을 사용하여 다시 시도한다는 것입니다. 검색시 키가 동일하지 않으면 동일한 수식을 사용하여 다음 오프셋을 찾습니다. 그것에 대해 무작위가 없습니다.
— 에반 캐롤

20

편집 : 아래의 대답은 해시 충돌을 처리 할 수있는 방법 중 하나입니다, 그러나 그것은 것입니다 하지 파이썬은 어떻게하는지. 아래 참조 된 Python의 위키도 올바르지 않습니다. 아래 @Duncan이 제공하는 최고의 소스는 구현 자체입니다. https://github.com/python/cpython/blob/master/Objects/dictobject.c 혼합에 대해 사과드립니다.


해시에 요소 목록 (또는 버킷)을 저장 한 다음 해당 목록에서 실제 키를 찾을 때까지 해당 목록을 반복합니다. 사진은 천 단어 이상을 말합니다.

해시 테이블

여기에 당신이 볼 John Smith과 Sandra Dee에 모두 해시 152. 버킷 152에는 둘 다 포함됩니다. 조회 할 Sandra Dee때 먼저 버킷에서 목록을 찾은 152다음 Sandra Dee찾을 때까지 해당 목록을 반복하여 반환합니다 521-6955.

다음은이 상황 만 여기에 잘못된 : 에 파이썬의 위키는 파이썬이 조회를 수행하는 방법 (? 의사) 코드를 찾을 수 있습니다.

실제로이 문제에 대한 몇 가지 가능한 해결책이 있습니다. wikipedia 문서에서 멋진 개요를 확인하세요. http://en.wikipedia.org/wiki/Hash_table#Collision_resolution


설명을 해주셔서 특히 의사 코드가있는 Python 위키 항목에 대한 링크에 감사드립니다!
— Praveen Gollakota 2012 년

2
미안하지만이 대답은 아주 틀 렸습니다 (위키 기사도 마찬가지입니다). 파이썬은 해시에 요소 목록이나 버킷을 저장하지 않습니다. 해시 테이블의 각 슬롯에 정확히 하나의 객체를 저장합니다. 처음 사용하려는 슬롯이 점유 된 경우 다른 슬롯 (가능한 한 오래 사용하지 않는 해시 부분을 끌어 당김)을 선택한 다음 다른 슬롯을 선택합니다. 해시 테이블이 1/3 이상 채워지지 않았으므로 결국 사용 가능한 슬롯을 찾아야합니다.
— Duncan

@Duncan, Python의 wiki는 이것이 이런 방식으로 구현되었다고 말합니다. 더 나은 소스를 찾을 수 있으면 좋겠습니다. wikipedia.org 페이지는 분명히 틀린 것이 아니라 언급 된 가능한 해결책 중 하나 일뿐입니다.
— Rob Wouters 2012 년

@Duncan 설명해 주시겠습니까? 해시의 사용하지 않는 부분을 가능한 한 오래 가져 오시겠습니까? 제 경우의 모든 해시는 42로 평가됩니다. 감사합니다!
— Praveen Gollakota

@PraveenGollakota 해시가 사용되는 방법을 자세히 설명하는 내 대답의 링크를 따르십시오. 해시가 42이고 슬롯이 8 개인 테이블의 경우 처음에는 가장 낮은 3 비트 만 슬롯 번호 2를 찾는 데 사용되지만 해당 슬롯이 이미 사용 된 경우 나머지 비트가 작동합니다. 두 값이 정확히 동일한 해시를 갖는 경우 첫 번째는 시도한 첫 번째 슬롯에 들어가고 두 번째 값은 다음 슬롯을 얻습니다. 동일한 해시를 가진 1000 개의 값이있는 경우 값을 찾기 전에 1000 개의 슬롯을 시도하게되며 사전 조회가 매우 느려집니다!
— Duncan

4

일반적으로 해시 테이블은 해시 충돌을 허용해야합니다! 당신은 불행해질 것이고 두 가지가 결국 같은 것을 해시 할 것입니다. 그 아래에는 동일한 해시 키를 가진 항목 목록에 개체 집합이 있습니다. 일반적으로 해당 목록에는 한 가지만 있지만이 경우 동일한 항목에 계속 쌓입니다. 그들이 다르다는 것을 아는 유일한 방법은 같음 연산자를 통하는 것입니다.

이런 일이 발생하면 시간이 지남에 따라 성능이 저하되므로 해시 함수가 "가능한 한 무작위"가되기를 원합니다.


2

스레드에서 파이썬이 사용자 정의 클래스의 인스턴스를 키로 딕셔너리에 넣었을 때 정확히 무엇을하는지 보지 못했습니다. 몇 가지 문서를 읽어 보겠습니다. 해시 가능한 객체 만 키로 사용할 수 있다고 선언합니다. Hashable은 모두 변경 불가능한 내장 클래스와 모든 사용자 정의 클래스입니다.

사용자 정의 클래스에는 기본적으로 __cmp __ () 및 __hash __ () 메서드가 있습니다. 그들과 함께, 모든 객체는 같지 않다 (자신을 제외하고) 비교하고 x .__ hash __ ()는 id (x)에서 파생 된 결과를 반환합니다.

따라서 클래스에 지속적으로 __hash__가 있지만 __cmp__ 또는 __eq__ 메서드를 제공하지 않으면 모든 인스턴스가 사전과 같지 않습니다. 반면에 __cmp__ 또는 __eq__ 메서드를 제공하지만 __hash__를 제공하지 않는 경우 인스턴스는 사전 측면에서 여전히 동일하지 않습니다.

class A(object):
    def __hash__(self):
        return 42


class B(object):
    def __eq__(self, other):
        return True


class C(A, B):
    pass


dict_a = {A(): 1, A(): 2, A(): 3}
dict_b = {B(): 1, B(): 2, B(): 3}
dict_c = {C(): 1, C(): 2, C(): 3}

print(dict_a)
print(dict_b)
print(dict_c)

산출

{<__main__.A object at 0x7f9672f04850>: 1, <__main__.A object at 0x7f9672f04910>: 3, <__main__.A object at 0x7f9672f048d0>: 2}
{<__main__.B object at 0x7f9672f04990>: 2, <__main__.B object at 0x7f9672f04950>: 1, <__main__.B object at 0x7f9672f049d0>: 3}
{<__main__.C object at 0x7f9672f04a10>: 3}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.