Pythonの組み込み辞書はどのように実装されていますか?


294

誰もがPythonの組み込み辞書タイプがどのように実装されているか知っていますか?私の理解では、これはある種のハッシュテーブルですが、どのような決定的な答えも見つけることができませんでした。


4
これは、2.7から3.6までのPython辞書に関する洞察に富んだ話です。リンク
セーレン

回答:


494

ここに、私がまとめることができたPython dictに関するすべてがあります(おそらく誰もが知りたいと思っているよりも多いですが、答えは包括的です)。

  • 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は)比較し、ハッシュとキーを(比較I平均で==比較ないis(挿入する現在のエントリのハッシュとキーに対するスロット内のエントリの比較)dictobject.c :337,344-345)それぞれ。両方が一致する場合、エントリはすでに存在していると見なされ、あきらめて、挿入される次のエントリに進みます。ハッシュまたはキーのいずれかが一致しない場合、プローブが開始されます
  • プローブとは、スロットごとにスロットを検索して空のスロットを見つけることを意味します。技術的には、1つずつ進みi+1, i+2, ...、最初に利用可能なものを使用することができます(これは線形プローブです)。しかし、コメント(dictobject.c:33-126を参照)で美しく説明されている理由により、CPythonはランダムプローブを使用します。ランダムプローブでは、次のスロットが疑似ランダムな順序で選択されます。エントリが最初の空のスロットに追加されます。この説明では、次のスロットを選択するために使用される実際のアルゴリズムはそれほど重要ではありません(プローブのアルゴリズムについては、dictobject.c:33-126を参照してください)。重要なのは、最初の空のスロットが見つかるまでスロットがプローブされることです。
  • ルックアップでも同じことが起こり、最初のスロットiから始まります(iはキーのハッシュに依存します)。ハッシュとキーの両方がスロット内のエントリと一致しない場合は、一致するスロットが見つかるまでプローブが開始されます。すべてのスロットが使い果たされると、失敗を報告します。
  • ところで、dict3分の2がいっぱいになると、サイズが変更されます。これにより、検索の速度低下を回避できます。(dictobject.h:64-65を参照)

注:dictの複数のエントリが同じハッシュ値を持つことができるかどうかについての私自身の質問に応えて、Python Dictの実装に関する調査を行いました。すべての調査がこの質問にも非常に関連しているので、ここで少し編集した回答を投稿しました。


8
ハッシュとキーの両方が一致すると、それ(opを挿入)はあきらめて先に進みます。この場合、既存のエントリを上書きして挿入しませんか?
0xc0de 2018

65

Pythonの組み込み辞書はどのように実装されていますか?

ここに短いコースがあります:

  • それらはハッシュテーブルです。(Pythonの実装の詳細については、以下を参照してください。)
  • Python 3.6以降の新しいレイアウトとアルゴリズムにより、
    • キー挿入順
    • 省スペース、
    • 実質的にパフォーマンスを犠牲にすることなく。
  • 別の最適化では、dictsがキーを共有するときにスペースを節約します(特別な場合)。

順序付けされた側面は、Python 3.6の時点では非公式ですが(他の実装に追いつく機会を与えるため)、Python 3.7では公式です

Pythonの辞書はハッシュテーブルです

長い間、それはまさにこのように機能しました。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__こちらの私の回答を参照してください

以下も参照してください。


1
あなたは「私たち」と言い、「Pythonの他の実装に追いつく機会を与えるため」-これはあなたが「知っている」ことを意味し、それは永続的な機能になる可能性がありますか?仕様で順序付けされているdictsにマイナス面はありますか?
toonarmycaptain 2017

順序付けの欠点は、dictsが順序付けされることが予想される場合、順序付けされていないより良い/より速い実装に簡単に切り替えることができないことです。しかし、そうなる可能性は低いようです。私は多くの話を見て、コアメンバーや他の人が書いた多くのことを私よりも現実の評価が高いので「知っている」ので、引用できる情報源がすぐに入手できない場合でも、私は通常知っています。私が話していること。しかし、レイモンドヘッティンガーの講演の1つからその点を理解できると思います。
アーロンホール

1
あなたは挿入がどのように機能するかを漠然と説明しました(「ハッシュが既存のキーのハッシュと同じで終わった場合、...それはキーと値のペアを別の場所に貼り付けます」-ありますか)しかし、あなたは説明しませんでしたルックアップとメンバーシップテストの仕組み。場所がハッシュによってどのように決定されるかは明確ではありませんが、サイズは常に2の累乗であり、ハッシュの最後の数ビットを取ると思います...
Alexey

@Alexey私が提供する最後のリンクは、注釈付きのdict実装を提供します-github.com/python/cpython/blob/master/Objects/dictobject.cと呼ばれる、現在これを行う関数を見つけることができますfind_empty_slot#L969-そして134行目から、それを説明する散文があります。
アーロンホール

46

Python辞書はオープンアドレス指定を使用します(Beautifulコード内の参照

NB! オープンアドレッシング、別名クローズドハッシュは、ウィキペディアに記載されているように、反対のオープンハッシュと混同しないでください

オープンアドレス指定とは、dictが配列スロットを使用し、オブジェクトの主要な位置がdict内で取得されると、オブジェクトのスポットが、オブジェクトのハッシュ値が役割を果たす「摂動」スキームを使用して、同じ配列内の別のインデックスで検索されることを意味します。


5
「反対のオープンハッシュと混同しないでください!-あなたがそれを書いたときにどの回答が受け入れられたか、またはその回答がその時点で何を言ったかはわかりません。
Tony Delroy、2015
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.