Python dictが同じハッシュを持つ複数のキーを持つことができるのはなぜですか?


90

私は内部でPythonhash関数を理解しようとしています。すべてのインスタンスが同じハッシュ値を返すカスタムクラスを作成しました。

class C:
    def __hash__(self):
        return 42

上記のクラスのインスタンスは一度に1つしか存在できないと仮定しましたdictが、実際には、adictは同じハッシュを持つ複数の要素を持つことができます。

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__、クラスのすべてのインスタンスが等しく比較されるようにメソッドをオーバーライドすると、dict1つのインスタンスしか許可されないことがわかりました。

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
あなたが自分自身を発見したように、オブジェクトがそれ自体と等しくない場合、セットとdictは等しいハッシュを持つ複数のオブジェクトを含むことができます。何を聞いていますか?テーブルはどのように機能しますか?これは、既存の資料がたくさんある非常に一般的な質問です...

@delnan質問を投稿した後、私はこれについてもっと考えていました。この動作はPythonに限定することはできません。そして、あなたは正しいです。一般的なハッシュテーブルの文献をさらに深く掘り下げる必要があると思います。ありがとう。
Praveen Gollakota 2012年

回答:


55

Pythonのハッシュがどのように機能するかの詳細な説明については、なぜアーリーリターンが他より遅いのかに対する私の答えを参照してください

基本的には、ハッシュを使用してテーブル内のスロットを選択します。スロットに値があり、ハッシュが一致する場合、アイテムを比較して、それらが等しいかどうかを確認します。

ハッシュが一致しないか、アイテムが等しくない場合、別のスロットを試行します。これを選択する式があり(参照された回答で説明します)、ハッシュ値の未使用部分を徐々に取り込みます。しかし、それらをすべて使い切ると、最終的にはハッシュテーブルのすべてのスロットを通過します。これにより、最終的に一致するアイテムまたは空のスロットが見つかることが保証されます。検索で空のスロットが見つかると、値を挿入するか、あきらめます(値を追加するか取得するかによって異なります)。

注意すべき重要な点は、リストやバケットがないことです。特定の数のスロットを持つハッシュテーブルがあり、各ハッシュは候補スロットのシーケンスを生成するために使用されます。


7
ハッシュテーブルの実装について正しい方向を示してくれてありがとう。私はハッシュテーブルについてこれまでに読みたかったよりもはるかに多くを読み、別の回答で私の発見を説明しました。stackoverflow.com/a/9022664/553995
Praveen Gollakota 2012年

112

これが私がまとめることができたPythondictsに関するすべてです(おそらく誰もが知りたいよりも多いですが、答えは包括的です)。Pythonがスロットを使用するように指示し、私をこのうさぎの穴に導いてくれたことを指摘して、Duncanに叫びます。

  • Python辞書はハッシュテーブルとして実装されてます。
  • ハッシュテーブルはハッシュの衝突を考慮に入れる必要があります。つまり、2つのキーのハッシュ値が同じであっても、テーブルの実装には、キーと値のペアを明確に挿入および取得する戦略が必要です。
  • Python dictは、オープンアドレス法を使用しハッシュの衝突を解決します(以下で説明します)(dictobject.c:296-297を参照)。
  • Pythonハッシュテーブルは単なるメモリの連続ブロックです(配列のようなものなのでO(1)、インデックスでルックアップできます)。
  • テーブルの各スロットには、1つのエントリのみを格納できます。これは重要
  • テーブルの各エントリは、実際には3つの値の組み合わせです-。これはC構造体として実装されます(dictobject.h:51-56を参照)。
  • 次の図は、Pythonハッシュテーブルの論理表現です。次の図で、左側の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: 337344-345)。両方が一致する場合、エントリはすでに存在していると見なし、あきらめて次のエントリに移動して挿入します。ハッシュまたはキーのいずれかが一致しない場合、プローブを開始します
  • プロービングとは、スロットごとにスロットを検索して空のスロットを見つけることを意味します。技術的には、i + 1、i + 2、...を1つずつ実行し、最初に使用可能なもの(線形プロービング)を使用することができます。しかし、コメントで美しく説明されている理由(dictobject.c:33-126を参照)のために、CPythonはランダムプロービングを使用します。ランダムプロービングでは、次のスロットが疑似ランダム順序で選択されます。エントリは最初の空のスロットに追加されます。この説明では、次のスロットを選択するために使用される実際のアルゴリズムはそれほど重要ではありません(プロービングのアルゴリズムについては、dictobject.c:33-126を参照してください)。重要なのは、最初の空のスロットが見つかるまでスロットがプローブされることです。
  • ルックアップでも同じことが起こり、最初のスロットiから始まります(iはキーのハッシュに依存します)。ハッシュとキーの両方がスロットのエントリと一致しない場合、一致するスロットが見つかるまでプローブを開始します。すべてのスロットが使い果たされると、失敗が報告されます。
  • ところで、dictが3分の2いっぱいになると、サイズが変更されます。これにより、ルックアップの速度が低下するのを防ぎます。(dictobject.h:64-65を参照)

どうぞ!dictのPython実装は、==アイテムを挿入するときに、2つのキーのハッシュの同等性とキーの通常の同等性()の両方をチェックします。したがって、要約すると、2つのキー、aおよびb、およびhash(a)==hash(b)、がa!=b存在する場合、両方がPythondictに調和して存在する可能性があります。しかし、hash(a)==hash(b) との 場合a==b両方を同じdictにすることはできません。

ハッシュの衝突が発生するたびにプローブする必要があるため、ハッシュの衝突が多すぎると、ルックアップと挿入が非常に遅くなるという副作用があります(Duncanがコメントで指摘しているように)。

私の質問に対する簡単な答えは、「それがソースコードに実装されている方法だからです;)」だと思います。

これは知っておくと良いことですが(オタクのポイントについては?)、実際の生活でどのように使用できるかわかりません。何かを明示的に壊そうとしない限り、等しくない2つのオブジェクトが同じハッシュを持つのはなぜですか?


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バージョンはハッシュを変更して、意図的にこれを行うのを困難にします。
ダンカン

2
@Duncan Aが削除された後、Bでルックアップを実行するとどうなりますか?実際にエントリを削除するのではなく、削除済みとしてマークするのではないでしょうか。すなわち、.... dictsが連続挿入および削除には適していないことを意味する
GEN-YS

2
@ gen-ys yes削除されたものと使用されていないものは、ルックアップで異なる方法で処理されます。未使用は一致の検索を停止しますが、削除は停止しません。挿入時に、削除または未使用のいずれかが、使用可能な空のスロットとして扱われます。連続的な挿入と削除は問題ありません。未使用の(削除されていない)スロットの数が少なくなりすぎると、ハッシュテーブルは、現在のテーブルに対して大きくなりすぎた場合と同じ方法で再構築されます。
ダンカン

1
これは、ダンカンが修正しようとした衝突点についてはあまり良い答えではありません。あなたの質問から実装のために参照することは特に悪い答えです。これを理解する上で最も重要なことは、衝突が発生した場合、Pythonは数式を使用してハッシュテーブルの次のオフセットを計算しようとすることです。キーが同じでない場合の取得時に、同じ式を使用して次のオフセットを検索します。それについてランダムなことは何もありません。
エヴァンキャロル

20

編集:以下の答えは、ハッシュの衝突に対処するための可能な方法の1つですが、Pythonがそれを行う方法ではありませ。以下で参照されているPythonのwikiも正しくありません。以下の@Duncanによって提供される最良のソースは、実装自体です。https//github.com/python/cpython/blob/master/Objects/dictobject.c混乱をお詫びします。


要素のリスト(またはバケット)をハッシュに格納し、そのリストで実際のキーが見つかるまでそのリストを繰り返し処理します。写真は千以上の言葉を言います:

ハッシュ表

ここに表示されJohn SmithSandra Dee両方がにハッシュされ152ます。バケットに152は両方が含まれています。Sandra Dee検索すると、最初にバケット内の152リストSandra Deeが見つかり、次に、見つかるまでそのリストをループして、を返します521-6955

以下は間違っています。これはコンテキストのためだけです。Pythonのwikiで、Pythonがルックアップを実行する方法を(疑似?)コードで見つけることができます。

この問題には実際にいくつかの可能な解決策があります。ウィキペディアの記事で概要を確認してください。 http //en.wikipedia.org/wiki/Hash_table#Collision_resolution


説明、特に疑似コードを含むPython wikiエントリへのリンクをありがとう!
Praveen Gollakota 2012年

2
申し訳ありませんが、この答えはまったく間違っています(wikiの記事もそうです)。Pythonは、要素のリストまたはバケットをハッシュに格納しません。ハッシュテーブルの各スロットに正確に1つのオブジェクトを格納します。最初に使用しようとしたスロットが占有されている場合は、別のスロットを選択し(ハッシュの未使用部分を可能な限り長くプルします)、次に別のスロットを選択します。ハッシュテーブルが3分の1を超えていっぱいになることはないため、最終的には使用可能なスロットを見つける必要があります。
ダンカン

@ Duncan、Pythonのウィキはそれがこのように実装されていると言っています。より良い情報源を見つけていただければ幸いです。wikipedia.orgページは間違いなく間違いではありません、それは述べられたように可能な解決策の1つにすぎません。
Rob Wouters 2012年

@Duncan説明していただけますか...ハッシュの未使用部分をできるだけ長く引き込みますか?私の場合のすべてのハッシュは42と評価されます。ありがとう!
Praveen Gollakota 2012年

@PraveenGollakotaハッシュがどのように使用されるかを詳細に説明している、私の回答のリンクをたどってください。42のハッシュと8スロットのテーブルの場合、最初は下位3ビットのみがスロット番号2の検索に使用されますが、そのスロットがすでに使用されている場合は、残りのビットが機能します。2つの値のハッシュがまったく同じである場合、最初の値は試行された最初のスロットに入り、2番目の値は次のスロットを取得します。ハッシュが同じ1000個の値がある場合、値が見つかる前に1000スロットを試行することになり、辞書の検索が非常遅くなります。
ダンカン

4

ハッシュテーブルは、一般に、ハッシュの衝突を考慮に入れる必要があります。あなたは不運になり、2つのことが最終的に同じものにハッシュされます。その下には、同じハッシュキーを持つアイテムのリスト内のオブジェクトのセットがあります。通常、そのリストには1つしかありませんが、この場合、それらを同じものにスタックし続けます。それらが異なることを知る唯一の方法は、equals演算子を使用することです。

これが発生すると、パフォーマンスは時間の経過とともに低下します。そのため、ハッシュ関数をできるだけ「ランダム」にする必要があります。


2

スレッドでは、Pythonをキーとして辞書に入れたときに、ユーザー定義クラスのインスタンスを正確に処理する方法がわかりませんでした。いくつかのドキュメントを読んでみましょう。ハッシュ可能なオブジェクトのみをキーとして使用できると宣言しています。ハッシュ可能は、すべて不変の組み込みクラスとすべてのユーザー定義クラスです。

ユーザー定義クラスには、デフォルトで__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.