__hash __()を実装するための正しい正しい方法は何ですか?


150

実装するための正しい方法は__hash__()何ですか?

私は、ハッシュコードを返す関数について話しています。この関数は、オブジェクトをハッシュテーブル(辞書)に挿入するために使用されます。

__hash__()整数を返し、オブジェクトをハッシュテーブルに「ビニング」するために使用されるので、返される整数の値は、一般的なデータに対して(衝突を最小限に抑えるために)均一に分散する必要があると想定しています。そのような値を取得するための良い習慣は何ですか?衝突は問題ですか?私の場合、int、float、stringを保持するコンテナクラスとして機能する小さなクラスがあります。

回答:


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関数の因数分解によるわずかなオーバーヘッドを除いて、これはハッシュと同じくらい高速です。確かに、属性が整数であることがわかっていて、その数が多すぎない場合は、いくつかのホームロールハッシュを使用すると、わずかに速く実行できる可能性がありますが、十分に分散されない可能性があります。hash((self.attr_a, self.attr_b, self.attr_c))は、小さなsの作成が特別に最適化されているため、驚くほど高速(かつ正しい)でtupleあり、ハッシュを取得および結合する作業をCビルトインにプッシュします。これは通常、Pythonレベルのコードより高速です。
ShadowRanger

クラスAのオブジェクトが辞書のキーとして使用されていて、クラスAの属性が変更されると、そのハッシュ値も変更されるとします。それは問題を引き起こしませんか?
マトリックス氏

1
以下の@ loved.by.Jesusの回答で述べているように、ハッシュメソッドは変更可能なオブジェクトに対して定義またはオーバーライドしないでください(デフォルトで定義され、IDを使用して等価と比較を行います)。
マトリックス氏

@ミゲル、私は正確な問題に遭遇しました、何が起こるかは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キーまたはセット要素として使用されたときに、あるオブジェクトが別のオブジェクトを置き換えることはありません。

古い/悪いソリューション

に関するPythonのドキュメントで__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(...)は、結合される値を再配置できない場合(たとえば、型が異なり、そのためにの値がor に_a割り当てられ_bない_c場合など)に削除できます。


5
値の順序を変更すると衝突が発生するため、通常は属性をまっすぐにXORしたくありません。すなわち、hash(A(1, 2, 3))と等しくなるhash(A(3, 1, 2))(それらは、両方のハッシュは、任意の他の等しいだろうAの順列を使用してインスタンス12及び3その値として)。インスタンスが引数のタプルと同じハッシュを持たないようにするには、センチネル値を(クラス変数として、またはグローバルとして)作成し、ハッシュするタプルに含めます:return hash((_ sentinel 、self._a、self._b、self._c))
Blckknght 2013

1
isinstanceサブクラスのtype(self)オブジェクトがのオブジェクトと等しくなる可能性があるため、の使用には問題がある可能性がありますtype(self)。したがって、a Carとa Fordをaに追加すると、set()挿入の順序によっては、1つのオブジェクトのみが挿入される場合があります。さらに、a == bTrueであるb == aがFalse である状況に遭遇する場合があります。
MaratC、2015年

1
あなたのしているサブクラス化した場合B、あなたはそれに変更することができますisinstance(othr, B)
millerdev

7
考え:キータプルにクラスタイプを含めることができます。これにより、同じ属性のキーセットを持つ他のクラスが等しいと表示されなくなります:hash((type(self), self._a, self._b, self._c))
Ben Mosher

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はそれと同じ方法が、31の代わりに101を使用して行います
user229898

3
これらの数値を使用する理由は何ですか?101または31を選択する理由はありますか?
bigblind 2013年

1
これが素数乗数の説明です:stackoverflow.com/questions/3613102/…。101は、Paul Larsonの実験に基づいて、特にうまく機能しているようです。
George V. Reilly 2013年

4
Pythonは(hash * 1000003) XOR ord(c)、32ビットのラップアラウンド乗算を使用した文字列を使用します。[引用 ]
タイラーl 2013

4
たとえこれが真実であっても、組み込みのPython文字列型がすでに__hash__メソッドを提供しているため、このコンテキストでは実用的ではありません。自分でロールする必要はありません。問題は__hash__、典型的なユーザー定義クラス(組み込み型またはおそらく他のそのようなユーザー定義クラスを指す一連のプロパティを持つ)を実装する方法ですが、この回答ではまったく対処していません。
Mark Amery

3

質問の2番目の部分に答えてみます。

衝突はおそらくハッシュコード自体からではなく、ハッシュコードをコレクション内のインデックスにマッピングすることから発生します。たとえば、ハッシュ関数は1から10000までのランダムな値を返す可能性がありますが、ハッシュテーブルに32エントリしかない場合、挿入時に衝突が発生します。

さらに、衝突はコレクションによって内部的に解決されると思います。衝突を解決する方法はたくさんあります。最も単純な(そして最悪の)ことは、インデックスiに挿入するエントリが与えられた場合、空のスポットが見つかるまでiに1を追加してそこに挿入することです。その後、検索は同じように機能します。これにより、コレクション全体を走査して検索する必要があるエントリが存在する可能性があるため、一部のエントリの検索が非効率になります。

他の衝突解決方法では、アイテムを挿入してアイテムを分散させるときに、ハッシュテーブル内のエントリを移動して検索時間を短縮します。これにより、挿入時間が長くなりますが、挿入するよりも多く読むことを前提としています。エントリが特定の場所に集まるように、衝突するさまざまなエントリを試行して分岐する方法もあります。

また、コレクションのサイズを変更する必要がある場合は、すべてを再ハッシュするか、動的ハッシュ方式を使用する必要があります。

つまり、ハッシュコードの用途によっては、独自の衝突解決メソッドを実装する必要がある場合があります。それらをコレクションに格納しない場合は、非常に広い範囲のハッシュコードを生成するだけのハッシュ関数を使用することで問題を回避できるでしょう。その場合、メモリの問題に応じて、コンテナが必要以上に大きいことを確認できます(もちろん大きいほど良いです)。

あなたがもっと興味があるならここにいくつかのリンクがあります:

ウィキペディアの合体ハッシュ

ウィキペディアには、さまざまな衝突解決方法の要約もあります。

また、Tharpによる「File Organization And Processing」では、衝突解決方法の多くを幅広くカバーしています。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);

そのようなシステムは、実際に浮動小数点値を表すのではなく、単にビット値としてそれらを取得した場合、浮動小数点に対しても機能する可能性があります。

文字列については、ほとんど/まったくわかりません。


衝突があることは知っています。しかし、私はこれらがどのように扱われるかについての手がかりはありません。さらに、私の属性値の組み合わせは非常にまばらに分布しているため、スマートなソリューションを探していました。そして、どういうわけか私はどこかにベストプラクティスがあると期待していました。
user229898
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.