__hash __ ()를 구현하는 정확하고 좋은 방법은 무엇입니까?


150

구현하는 올바른 방법은 무엇입니까 __hash__()?

해시 코드를 반환하는 함수에 대해 이야기하고 나서 해시 테이블 (일명 사전)에 객체를 삽입하는 데 사용됩니다.

마찬가지로 __hash__()내가 반환 된 정수의 값이 균일하게 공통 데이터를 분산해야한다고 가정 반환 정수 및 해시 테이블에 "비닝 (binning)"객체에 사용됩니다 (충돌을 최소화하기 위해). 그러한 가치를 얻는 좋은 방법은 무엇입니까? 충돌이 문제입니까? 내 경우에는 작은 클래스가있어서 정수, 부동 소수점 및 문자열을 보유하는 컨테이너 클래스 역할을합니다.

답변:


185

쉽고 정확한 구현 방법 __hash__()은 키 튜플을 사용하는 것입니다. 특수 해시만큼 빠르지는 않지만 필요한 경우 C로 유형을 구현해야합니다.

다음은 해시와 평등에 키를 사용하는 예입니다.

class A:
    def __key(self):
        return (self.attr_a, self.attr_b, self.attr_c)

    def __hash__(self):
        return hash(self.__key())

    def __eq__(self, other):
        if isinstance(other, A):
            return self.__key() == other.__key()
        return NotImplemented

또한, 설명서__hash__ 에는 특정 상황에서 유용 할 수있는 자세한 정보가 있습니다.


1
__key함수 를 분해 할 때 발생하는 사소한 오버 헤드 외에도 해시 속도만큼이나 빠릅니다. 물론 속성이 정수로 알려져 있고 그 수가 너무 많지 않으면 홈 롤링 해시로 약간 더 빠르게 실행될 수 있지만 잘 분산되지는 않았을 것입니다. small 의 생성 이 특별히 최적화 되어 hash((self.attr_a, self.attr_b, self.attr_c))놀랍게도 빠르고 정확tuple 하며 해시를 C 내장으로 가져오고 결합하는 작업을 추진합니다. 일반적으로 Python 레벨 코드보다 빠릅니다.
ShadowRanger

클래스 A의 객체가 사전의 키로 사용되고 있고 클래스 A의 속성이 변경되면 해시 값도 변경된다고 가정 해 봅시다. 문제가되지 않습니까?
Mr 매트릭스

1
아래의 @ loved.by.Jesus의 답변에서 언급했듯이 변경 가능한 객체에 대해 해시 방법을 정의 / 재정의해서는 안됩니다 (기본적으로 정의되고 동등성과 비교를 위해 id를 사용함).
Mr 매트릭스

@ Miguel, 정확한 문제 가 발생했습니다 None. 키가 변경되면 사전이 반환 됩니다. 내가 해결 한 방법은 객체의 ID를 객체 대신 키로 저장하는 것입니다.
Jaswant P

@JaswantP Python은 기본적으로 객체의 id를 해시 가능 객체의 키로 사용합니다.
미스터 매트릭스

22

John Millikin은 다음과 유사한 솔루션을 제안했습니다.

class A(object):

    def __init__(self, a, b, c):
        self._a = a
        self._b = b
        self._c = c

    def __eq__(self, othr):
        return (isinstance(othr, type(self))
                and (self._a, self._b, self._c) ==
                    (othr._a, othr._b, othr._c))

    def __hash__(self):
        return hash((self._a, self._b, self._c))

이 솔루션의 문제점은입니다 hash(A(a, b, c)) == hash((a, b, c)). 다시 말해, 해시는 주요 멤버의 튜플의 해시와 충돌합니다. 어쩌면 이것이 실제로 자주 문제가되지 않습니까?

업데이트 : Python 문서는 위의 예와 같이 튜플을 사용하는 것이 좋습니다. 설명서에 나와 있습니다.

유일하게 필요한 속성은 동일하게 비교되는 객체의 해시 값이 동일하다는 것입니다

그 반대는 사실이 아닙니다. 동일하게 비교되지 않은 객체 는 동일한 해시 값을 가질 있습니다. 이러한 해시 충돌 로 인해 객체가 동일하게 비교되지 않는 한 dict 키 또는 세트 요소 사용될 때 한 객체가 다른 객체를 대체 하지 않습니다 .

오래된 / 나쁜 솔루션

에 파이썬 문서는__hash__ XOR 같은 것을 사용하여 하위 구성 요소의 해시를 결합하는 제안 이 우리에게주는 :

class B(object):

    def __init__(self, a, b, c):
        self._a = a
        self._b = b
        self._c = c

    def __eq__(self, othr):
        if isinstance(othr, type(self)):
            return ((self._a, self._b, self._c) ==
                    (othr._a, othr._b, othr._c))
        return NotImplemented

    def __hash__(self):
        return (hash(self._a) ^ hash(self._b) ^ hash(self._c) ^
                hash((self._a, self._b, self._c)))

업데이트 : Blckknght가 지적했듯이 a, b 및 c의 순서를 변경하면 문제가 발생할 수 있습니다. ^ hash((self._a, self._b, self._c))해시되는 값의 순서를 캡처하기 위해 추가 기능 을 추가했습니다 . 이 마지막이 ^ hash(...)되는 조합 된 값을 재 배열 할 수없는 경우에 제거 될 수있다 (예를 들어, 따라서 서로 다른 유형 및 값이있는 경우 _a에 할당되지 않을 _b또는 _c등).


5
일반적으로 값의 순서를 변경하면 충돌이 발생하기 때문에 속성을 함께 직접 XOR하고 싶지 않습니다. 즉, hash(A(1, 2, 3))동일 할 것이다 hash(A(3, 1, 2))(이들은 모두 해시 다른 동등한 것이다 A의 순열 인스턴스 1, 23그 값으로). 인스턴스가 인수의 튜플과 동일한 해시를 갖지 않으려면 단순히 센티넬 값 (클래스 변수 또는 전역으로)을 만든 다음 해시 할 튜플에 포함하십시오 : return hash ((_ sentinel , self._a, self._b, self._c))
Blckknght

1
isinstance하위 클래스의 객체가 type(self)이제의 객체와 같을 수 있으므로 사용에 문제가있을 수 있습니다 type(self). 따라서 삽입 순서에 따라 a Car와 a Ford를 추가하면 set()하나의 객체 만 삽입 될 수 있습니다. 또한 a == bTrue이지만 b == aFalse 인 상황이 발생할 수 있습니다 .
MaratC

1
만약 당신이 서브 클래 싱을한다면 B, isinstance(othr, B)
이것을

7
생각 : 키 튜플에는 클래스 유형이 포함될 수 있으며, 이는 동일한 키 속성 세트를 가진 다른 클래스가 동일하게 표시되지 않도록 hash((type(self), self._a, self._b, self._c))합니다.
벤 모셔

2
B대신 에 사용에 대한 요점 외에도 에서 대신 예기치 않은 유형이 발생할 때 type(self)반환하는 것이 더 나은 방법으로 간주됩니다 . 이를 통해 다른 사용자 정의 유형은 원하는 경우 알고 있고 동일한 유형을 비교할 수 있습니다. NotImplemented__eq__False__eq__B
Mark Amery

16

Microsoft Research의 Paul Larson은 다양한 해시 함수를 연구했습니다. 그는 나에게 말했다

for c in some_string:
    hash = 101 * hash  +  ord(c)

다양한 현에서 놀랍도록 잘 작동했습니다. 비슷한 다항식 기술이 서로 다른 하위 필드의 해시를 계산하는 데 효과적이라는 것을 알았습니다.


8
분명히 Java는 동일한 방식을 사용하지만 101 대신 31을 사용합니다.
user229898

3
이 숫자를 사용하는 이유는 무엇입니까? 101 또는 31을 선택해야 할 이유가 있습니까?
bigblind

1
다음은 주요 승수에 대한 설명입니다 : stackoverflow.com/questions/3613102/... . 101은 Paul Larson의 실험을 바탕으로 특히 잘 작동하는 것 같습니다.
George V. Reilly

4
파이썬은 (hash * 1000003) XOR ord(c)32 비트 랩 어라운드 곱셈을 가진 문자열에 사용 합니다. [Citation ]
tylerl

4
이것이 사실이더라도 내장 된 파이썬 문자열 타입은 이미 __hash__메소드를 제공하기 때문에이 컨텍스트에서는 실용적이지 않습니다 . 우리는 우리 자신을 굴릴 필요가 없습니다. 문제는 __hash__전형적인 사용자 정의 클래스 (내장 유형 또는 다른 사용자 정의 클래스를 가리키는 많은 속성이있는) 를 구현하는 방법 입니다.이 답변은 전혀 다루지 않습니다.
Mark Amery

3

나는 당신의 질문의 두 번째 부분에 대답하려고 노력할 수 있습니다.

충돌은 아마도 해시 코드 자체가 아니라 해시 코드를 컬렉션의 인덱스에 매핑함으로써 발생합니다. 예를 들어 해시 함수는 1에서 10000 사이의 임의의 값을 반환 할 수 있지만 해시 테이블에 32 개의 항목 만 있으면 삽입시 충돌이 발생합니다.

또한 컬렉션 내부에서 충돌이 해결 될 것이라고 생각하며 충돌을 해결하는 방법에는 여러 가지가 있습니다. 가장 간단한 (그리고 최악의) 인덱스 i에 삽입 할 항목이 주어지면 빈 지점을 찾아서 삽입 할 때까지 1을 i에 추가하십시오. 그런 다음 검색도 같은 방식으로 작동합니다. 결과적으로 전체 컬렉션을 순회해야하는 항목이있을 수 있으므로 일부 항목에 대한 검색이 비효율적입니다.

다른 충돌 해결 방법은 항목을 삽입하여 항목을 분산시킬 때 해시 테이블의 항목을 이동하여 검색 시간을 줄입니다. 이것은 삽입 시간을 늘리지 만 삽입 한 것보다 더 많이 읽은 것으로 가정합니다. 하나의 특정 지점에서 항목을 클러스터링하기 위해 서로 다른 충돌 항목을 시도하고 분기하는 방법도 있습니다.

또한 컬렉션의 크기를 조정해야하는 경우 모든 항목을 다시 해시하거나 동적 해싱 방법을 사용해야합니다.

요컨대, 해시 코드를 사용하는 것에 따라 자체 충돌 해결 방법을 구현해야 할 수도 있습니다. 컬렉션에 저장하지 않으면 매우 넓은 범위에서 해시 코드를 생성하는 해시 함수를 사용하여 벗어날 수 있습니다. 그렇다면 메모리 문제에 따라 컨테이너가 필요한 것보다 커야합니다 (물론 클수록 좋습니다).

더 관심이 있다면 다음 링크를 참조하십시오.

Wikipedia의 통합 해싱

Wikipedia에는 다양한 충돌 해결 방법 이 요약 되어 있습니다.

또한 Tharp의 " 파일 구성 및 처리 "는 많은 충돌 해결 방법을 광범위하게 다루고 있습니다. IMO 해싱 알고리즘에 대한 훌륭한 참조 자료입니다.


1

__hash__함수 를 언제 그리고 어떻게 구현하는지에 대한 아주 좋은 설명 은 programiz 웹 사이트있습니다 :

개요를 제공하기위한 스크린 샷 : (2019-12-13 검색)

https://www.programiz.com/python-programming/methods/built-in/hash의 스크린 샷 2019-12-13

이 방법의 개인 구현과 관련하여 위에서 언급 한 사이트는 millerdev 의 답변과 일치하는 예제를 제공합니다 .

class Person:
def __init__(self, age, name):
    self.age = age
    self.name = name

def __eq__(self, other):
    return self.age == other.age and self.name == other.name

def __hash__(self):
    print('The hash is:')
    return hash((self.age, self.name))

person = Person(23, 'Adam')
print(hash(person))

0

반환하는 해시 값의 크기에 따라 다릅니다. 4 개의 32 비트 정수의 해시를 기반으로 32 비트 정수를 반환해야하는 경우 충돌이 발생한다는 간단한 논리입니다.

비트 작업을 선호합니다. 다음과 같은 C 의사 코드와 같이

int a;
int b;
int c;
int d;
int hash = (a & 0xF000F000) | (b & 0x0F000F00) | (c & 0x00F000F0 | (d & 0x000F000F);

부동 소수점 값을 실제로 나타내는 것이 아니라 단순히 비트 값으로 가져간 경우 이러한 시스템은 부동 소수점에도 사용할 수 있습니다.

문자열의 경우, 나는 거의 / 전혀 모른다.


충돌이 발생한다는 것을 알고 있습니다. 그러나 나는 이것들이 어떻게 처리되는지 전혀 모른다. 그리고 내 속성 값을 조합하여 매우 희소하게 배포하므로 스마트 솔루션을 찾고있었습니다. 어쨌든 나는 어딘가에 모범 사례가있을 것으로 기대했습니다.
229898
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.