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