回答:
ここに、私がまとめることができたPython dictに関するすべてがあります(おそらく誰もが知りたいと思っているよりも多いですが、答えは包括的です)。
dict
はオープンアドレス指定を使用してハッシュの衝突を解決します(以下で説明)(dictobject.c:296-297を参照)。O(1)
、インデックスでルックアップできます)。次の図は、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>
)。しかし、そのスロットが使用されている場合はどうなりますか?ほとんどの場合、別のエントリが同じハッシュを持っているため(ハッシュ衝突!)==
比較ないis
(挿入する現在のエントリのハッシュとキーに対するスロット内のエントリの比較)dictobject.c :337,344-345)それぞれ。両方が一致する場合、エントリはすでに存在していると見なされ、あきらめて、挿入される次のエントリに進みます。ハッシュまたはキーのいずれかが一致しない場合、プローブが開始されます。i+1, i+2, ...
、最初に利用可能なものを使用することができます(これは線形プローブです)。しかし、コメント(dictobject.c:33-126を参照)で美しく説明されている理由により、CPythonはランダムプローブを使用します。ランダムプローブでは、次のスロットが疑似ランダムな順序で選択されます。エントリが最初の空のスロットに追加されます。この説明では、次のスロットを選択するために使用される実際のアルゴリズムはそれほど重要ではありません(プローブのアルゴリズムについては、dictobject.c:33-126を参照してください)。重要なのは、最初の空のスロットが見つかるまでスロットがプローブされることです。dict
3分の2がいっぱいになると、サイズが変更されます。これにより、検索の速度低下を回避できます。(dictobject.h:64-65を参照)注:dictの複数のエントリが同じハッシュ値を持つことができるかどうかについての私自身の質問に応えて、Python Dictの実装に関する調査を行いました。すべての調査がこの質問にも非常に関連しているので、ここで少し編集した回答を投稿しました。
Pythonの組み込み辞書はどのように実装されていますか?
ここに短いコースがあります:
順序付けされた側面は、Python 3.6の時点では非公式ですが(他の実装に追いつく機会を与えるため)、Python 3.7では公式です。
長い間、それはまさにこのように機能しました。Pythonは8つの空の行を事前に割り当て、ハッシュを使用してキーと値のペアを貼り付ける場所を決定します。たとえば、キーのハッシュが001で終わっていた場合、(以下の例のように)1(つまり2番目)のインデックスに貼り付けられます。
<hash> <key> <value>
null null null
...010001 ffeb678c 633241c4 # addresses of the keys and values
null null null
... ... ...
各行は、64ビットアーキテクチャでは24バイト、32ビットアーキテクチャでは12バイトを占めます。(列ヘッダーは、ここでの目的のための単なるラベルであることに注意してください-実際にはメモリに存在しません。)
ハッシュが既存のキーのハッシュと同じように終了した場合、これは衝突であり、キーと値のペアを別の場所に貼り付けます。
5つのキーと値が格納された後、別のキーと値のペアを追加すると、ハッシュの衝突の確率が高すぎるため、辞書のサイズが2倍になります。64ビットプロセスでは、サイズ変更前に72バイトが空になり、その後、10行の空行のために240バイトが無駄になっています。
これには多くのスペースが必要ですが、ルックアップ時間はかなり一定です。キー比較アルゴリズムは、ハッシュを計算し、予想される場所に移動し、キーのIDを比較します。それらが同じオブジェクトである場合、それらは等しいです。その後、ハッシュ値を比較していない場合、彼らはしている場合は、ないと同じ、彼らは同じではないです。それ以外の場合は、最後にキーが等しいかどうかを比較し、等しい場合は値を返します。等式の最終比較はかなり遅くなる可能性がありますが、以前のチェックは通常、最終比較を簡略化し、検索を非常に高速にします。
衝突は物事を遅くし、攻撃者は理論的にハッシュ衝突を使用してサービス拒否攻撃を実行する可能性があるため、新しいPythonプロセスごとに異なるハッシュを計算するようにハッシュ関数の初期化をランダム化しました。
上記の無駄なスペースにより、辞書の実装を変更するようになりました。これは、辞書が挿入によって順序付けられるというエキサイティングな新機能です。
代わりに、挿入のインデックスに配列を事前に割り当てることから始めます。
最初のキーと値のペアは2番目のスロットに入るので、次のようにインデックスを作成します。
[null, 0, null, null, null, null, null, null]
そして、私たちのテーブルは挿入順で入力されます:
<hash> <key> <value>
...010001 ffeb678c 633241c4
... ... ...
したがって、キーの検索を行うときは、ハッシュを使用して予想される位置を確認し(この場合は、配列のインデックス1に直接移動します)、ハッシュテーブルのそのインデックスに移動します(たとえば、インデックス0 )、キーが等しいことを確認し(前述の同じアルゴリズムを使用)、等しい場合は値を返します。
一定のルックアップ時間を維持し、場合によってはわずかな速度損失と他の場合の利益を伴いますが、既存の実装よりもかなり多くのスペースを節約し、挿入順序を維持するという利点があります。無駄なスペースは、インデックス配列のnullバイトだけです。
レイモンドヘッティンガーは、2012年12月にpython-devでこれを導入しました。ついにPython 3.6の CPythonに入りました。挿入による順序付けは、3.6の実装の詳細と見なされ、Pythonの他の実装が追いつく機会を与えました。
スペースを節約するためのもう1つの最適化は、キーを共有する実装です。したがって、そのすべてのスペースを占める冗長な辞書を使用する代わりに、共有キーとキーのハッシュを再利用する辞書があります。次のように考えることができます。
hash key dict_0 dict_1 dict_2...
...010001 ffeb678c 633241c4 fffad420 ...
... ... ... ... ...
64ビットマシンの場合、これにより、追加の辞書ごとにキーあたり最大16バイトを節約できます。
これらの共有キー辞書は、カスタムオブジェクトに使用することを目的としています__dict__
。この動作を実現するには__dict__
、次のオブジェクトをインスタンス化する前に、への入力を完了する必要があると思います(PEP 412を参照)。つまり、__init__
または__new__
ですべての属性を割り当てる必要があります。そうしないと、スペースを節約できない可能性があります。
ただし、__init__
実行時にすべての属性がわかっている場合__slots__
は、オブジェクトを提供して、__dict__
まったく作成されないことを保証するか(親で使用できない場合)、または許可__dict__
されていても、予測される属性がとにかくスロットに格納されます。詳細については__slots__
、こちらの私の回答を参照してください。
**kwargs
関数内での順序の保持。find_empty_slot
。#L969-そして134行目から、それを説明する散文があります。
Python辞書はオープンアドレス指定を使用します(Beautifulコード内の参照)
NB! オープンアドレッシング、別名クローズドハッシュは、ウィキペディアに記載されているように、反対のオープンハッシュと混同しないでください。
オープンアドレス指定とは、dictが配列スロットを使用し、オブジェクトの主要な位置がdict内で取得されると、オブジェクトのスポットが、オブジェクトのハッシュ値が役割を果たす「摂動」スキームを使用して、同じ配列内の別のインデックスで検索されることを意味します。