web-dev-qa-db-ja.com

どうすればPython dictは同じハッシュを持つ複数のキーを持つことができますか?

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

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

上記のクラスのインスタンスはいつでも1つのセットにしか存在できないと想定しましたが、実際には、同じハッシュを持つ複数の要素をセットに含めることができます。

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

もう少し実験して、クラスのすべてのインスタンスが等しくなるように__eq__メソッドをオーバーライドすると、セットでは1つのインスタンスしか許可されないことがわかりました。

class D(C):
    def __eq__(self, other):
        return hash(self) == hash(other)

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

だから私は辞書が同じハッシュを持つ複数の要素を持つことができる方法を知りたいです。ありがとう!

注:回答のすべての議論は辞書に関するものであるため、質問を編集して(セットではなく)辞書の例を示しました。ただし、セットにも同じことが当てはまります。セットは、同じハッシュ値を持つ複数の要素を持つこともできます。

78

Pythonのハッシュがどのように機能するかの詳細な説明については、 なぜ早期リターンが他より遅いのですか?

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

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

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

42
Duncan

Python私が組み立てることができた辞書(おそらく誰もが知りたいと思うよりも多い;しかし、答えは包括的です)についてのすべてです。 ダンカン = Python dictsはスロットを使用し、このうさぎの穴に私を導きます。

  • Python辞書は、ハッシュテーブルとして実装されます。
  • ハッシュテーブルはハッシュ衝突を許可する必要があります。つまり、2つのキーが同じハッシュ値を持っている場合でも、テーブルの実装にはキーを挿入および取得する戦略が必要ですと値のペアは明確に。
  • Python dictはopen addressingを使用してハッシュ衝突を解決します(以下で説明)( dictobject.c:296-297 を参照)。
  • Pythonハッシュテーブルは単なるメモリの連続ブロックです(配列のようなものなので、インデックスでO(1)ルックアップを実行できます)。
  • テーブル内の各スロットには、エントリを1つだけ格納できます。これは重要です
  • テーブル内の各entryは、実際には3つの値の組み合わせです-。これはC構造体として実装されます( dictobject.h:51-56 を参照)
  • 次の図は、pythonハッシュテーブルの論理表現です。次の図では、左側の0、1、...、i、...は、slotsハッシュテーブル(これらは単に説明のためのものであり、テーブルと一緒に保存されないことは明らかです!)。

    _# Logical model of Python Hash table
    -+-----------------+
    0| <hash|key|value>|
    -+-----------------+
    1|      ...        |
    -+-----------------+
    .|      ...        |
    -+-----------------+
    i|      ...        |
    -+-----------------+
    .|      ...        |
    -+-----------------+
    n|      ...        |
    -+-----------------+
    _
  • 新しい辞書が初期化されると、8slotsで始まります。 ( dictobject.h:49 を参照)

  • テーブルにエントリを追加するときは、キーのハッシュに基づくiスロットから始めます。 CPythonは初期のi = hash(key) & maskを使用します。ここで_mask = PyDictMINSIZE - 1_ですが、それはそれほど重要ではありません)。チェックされる最初のスロットiは、キーのhashに依存することに注意してください。
  • そのスロットが空の場合、エントリはスロットに追加されます(エントリごとに、つまり_<hash|key|value>_)。しかし、そのスロットが占有されている場合はどうでしょう!?ほとんどの場合、別のエントリが同じハッシュを持っているためです(ハッシュ衝突!)
  • スロットが占有されている場合、CPython(およびPyPyも)はハッシュとキーを比較します(比較により、私は_==_比較ではなく挿入する現在のエントリのキーに対するスロット内のエントリのis比較)( dictobject.c:33744-345 )。 bothが一致する場合、エントリはすでに存在するとみなし、あきらめて、挿入する次のエントリに進みます。ハッシュまたはキーのいずれかが一致しない場合、probingが開始されます。
  • プローブとは、スロットごとにスロットを検索して空のスロットを見つけることを意味します。技術的には、i + 1、i + 2、...を1つずつ実行し、最初に使用可能なものを使用することができます(線形プローブ)。しかし、コメントで美しく説明されている理由により( dictobject.c:33-126 を参照)、CPythonはランダムプローブを使用します。ランダムプローブでは、次のスロットが擬似ランダムな順序で選択されます。エントリは最初の空のスロットに追加されます。この説明では、次のスロットを選択するために使用される実際のアルゴリズムはあまり重要ではありません(プロービングのアルゴリズムについては dictobject.c:33-126 を参照してください)。重要なのは、最初の空のスロットが見つかるまでスロットがプローブされることです。
  • ルックアップでも同じことが起こります。最初のスロットiから始まります(iはキーのハッシュに依存します)。ハッシュとキーの両方がスロット内のエントリと一致しない場合、一致するスロットが見つかるまでプローブが開始されます。すべてのスロットが使い果たされると、失敗が報告されます。
  • ところで、辞書の3分の2がいっぱいになると、辞書のサイズが変更されます。これにより、ルックアップが遅くなるのを防ぎます。 ( dictobject.h:64-65 を参照)

行くぞ! Python dictの実装は、アイテムを挿入するときに2つのキーのハッシュ等価性とキーの通常の等価性(_==_)の両方をチェックします。 abhash(a)==hash(b)ですが、_a!=b_の場合、両方がPython dictに調和して存在できます。 hash(a)==hash(b)and_a==b_、両方を同じ辞書に入れることはできません。

ハッシュ衝突のたびにプローブする必要があるため、ハッシュ衝突が多すぎる場合の副作用の1つは、ルックアップと挿入が非常に遅くなることです(ダンカンが comments で指摘しているように)。

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

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

94

Edit:以下の答えはハッシュ衝突に対処するための可能な方法の1つですが、not方法Pythonそれを実行します。以下で参照するPythonのwikiも正しくありません。以下の@Duncanが提供する最適なソースは実装自体です: http:// svn .python.org/projects/python/trunk/Objects/dictobject.c 混乱して申し訳ありません。


ハッシュに要素のリスト(またはバケット)を保存し、そのリストで実際のキーが見つかるまでそのリストを反復処理します。写真には1000以上の言葉があります:

Hash table

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

以下は間違っていますが、ここではコンテキストのみです:On Python's wiki you can find(pseudo?)code how Pythonはルックアップを実行します。

実際、この問題にはいくつかの解決策があります。ニースの概要については、ウィキペディアの記事をご覧ください。 http://en.wikipedia.org/wiki/Hash_table#Collision_resolution

19
Rob Wouters

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

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

4
Donald Miner

スレッドでは、キーとして辞書に入れたときにユーザー定義クラスのインスタンスで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}
2
checkraise