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 구현은 ==항목을 삽입 할 때 두 키의 해시 같음과 키의 정상 같음 ( ) 을 모두 확인 합니다. 요약하면, 두 개의 키 abhash(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 SmithSandra 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.