web-dev-qa-db-ja.com

辞書とセットの順序はなぜarbitrary意的ですか?

辞書をループしたり、pythonで設定)が '任意'の順序でどのように行われるかがわかりません。

つまり、プログラミング言語なので、言語のすべてを100%決定する必要があります、正しいですか? Pythonには、辞書またはセットのどの部分を選択するか、1番目、2番目などを決定する何らかのアルゴリズムが必要です。

私は何が欠けていますか?

142

順序は任意ではありませんが、特定のPython実装だけでなく、辞書またはセットの挿入および削除の履歴によって異なります。この回答の残りの部分では、「辞書」については「セット」も読むことができます。セットは、キーのみで値のない辞書として実装されます。

キーはハッシュされ、ハッシュ値は動的テーブルのスロットに割り当てられます(ニーズに応じて拡大または縮小できます)。そして、そのマッピングプロセスは衝突につながる可能性があります。つまり、すでに存在するものに基づいてnextスロットにキーをスロットする必要があります。

内容がスロットにループするので、キーはcurrentlyがテーブルに存在する順にリストされます。

たとえば、キー_'foo'_および_'bar'_を使用し、テーブルサイズが8スロットであると仮定します。 Python 2.7では、hash('foo')は_-4177197833195190597_、hash('bar')は_327024216814240868_です。 8を法とする、つまり、これら2つのキーがスロット3と4にスロットされていることを意味します。

_>>> hash('foo')
-4177197833195190597
>>> hash('foo') % 8
3
>>> hash('bar')
327024216814240868
>>> hash('bar') % 8
4
_

これにより、リストの順序が通知されます。

_>>> {'bar': None, 'foo': None}
{'foo': None, 'bar': None}
_

3と4を除くすべてのスロットは空で、テーブルをループすると最初にスロット3がリストされ、次にスロット4がリストされるため、_'foo'_が_'bar'_の前にリストされます。

ただし、barおよびbazには、正確に8離れたハッシュ値があるため、まったく同じスロット_4_にマップされます。

_>>> hash('bar')
327024216814240868
>>> hash('baz')
327024216814240876
>>> hash('bar') % 8
4
>>> hash('baz') % 8
4
_

それらの順序は、どのキーが最初にスロット化されたかによって異なります。 2番目のキーは次のスロットに移動する必要があります。

_>>> {'baz': None, 'bar': None}
{'bar': None, 'baz': None}
>>> {'bar': None, 'baz': None}
{'baz': None, 'bar': None}
_

ここでは、どちらかのキーが最初にスロットに入れられたため、テーブルの順序が異なります。

CPython(最も一般的に使用されるPython実装)で使用される基礎となる構造の技術名は、オープンアドレス指定を使用する ハッシュテーブル です。好奇心が強く、Cを十分に理解している場合は、すべての(十分に文書化された)詳細について C implementation を見てください。また、これを見ることができます Brandon RhodesによるPycon 2010のプレゼンテーション CPython dictの仕組みについて、または Beautiful Code のコピーを入手して、 Andrew Kuchlingによって書かれた実装。

Python 3.3以降、ランダムハッシュシードも使用されるため、特定のタイプのサービス拒否(攻撃者がPythonサーバーを大量のハッシュ衝突を引き起こします)。つまり、指定された辞書の順序は、also現在のPython呼び出しのランダムハッシュシードに依存します。

他の実装では、文書化されたPythonインターフェースを満たす限り、辞書に異なる構造を自由に使用できますが、これまでの実装はすべてハッシュテーブルのバリエーションを使用していると思います。

CPython 3.6では、挿入順序を維持し、起動がより高速でメモリ効率が高いnewdict実装が導入されています。新しい実装では、各行が保存されたハッシュ値とキーおよび値オブジェクトを参照する大きなスパーステーブルを保持するのではなく、密なテーブルのインデックスのみを参照する小さなハッシュarrayを追加します(実際のキーと値のペアと同じ数の行のみを含むもの)、そして含まれるアイテムを順番にリストするのはたまたま密なテーブルです。 詳細についてはPython-Devの提案 を参照してください。 Python 3.6では、これは実装の詳細と見なされることに注意してください。Python言語では、他の実装が順序を保持する必要があることを指定していません。これはPython 3.7で変更されました。この詳細は 言語仕様 ;実装がPython 3.7以降と適切に互換性を持つためにはmustこの順序を維持する動作をコピーしてください。

Python 2.7以降では、 OrderedDict class も提供されます。これは、キーの順序を記録するための追加のデータ構造を追加するdictのサブクラスです。ある程度の速度と追加のメモリを犠牲にして、このクラスはキーを挿入した順序を記憶します。キー、値、またはアイテムをリストすることは、その順番でそうします。追加の辞書に保存された二重リンクリストを使用して、効率的に順序を最新に保ちます。 アイデアの概要を説明するレイモンド・ヘッティンガーによる投稿 を参照してください。 setタイプはまだ順序付けられていないことに注意してください。

順序付きセットが必要な場合は、 oset package をインストールできます。 Python 2.5以降で動作します。

224
Martijn Pieters

これは、重複として閉じられる前の Python 3.41 Aセット に対する応答です。


他の人は正しい:順序に依存しないでください。あるふりをしてはいけません。

とはいえ、one信頼できるものがあります。

_list(myset) == list(myset)
_

つまり、順序はstableです。


perceivedの順序がある理由を理解するには、いくつかのことを理解する必要があります。

  • つまり、Pythonはハッシュセットを使用します。

  • CPythonのハッシュセットをメモリに保存する方法と

  • 数字がハッシュされる方法

上から:

ハッシュセットは、非常に高速なルックアップ時間でランダムデータを格納する方法です。

バッキング配列があります:

_# A C array; items may be NULL,
# a pointer to an object, or a
# special dummy object
_ _ 4 _ _ 2 _ _ 6
_

これらのセットからは削除しないため、削除の処理を容易にするためにのみ存在する特別なダミーオブジェクトは無視します。

非常に高速なルックアップを行うために、オブジェクトからハッシュを計算する魔法を実行します。唯一のルールは、等しい2つのオブジェクトが同じハッシュを持つことです。 (ただし、2つのオブジェクトのハッシュが同じ場合、等しくない可能性があります。)

次に、配列の長さでモジュラスを取ることにより、インデックスを作成します。

_hash(4) % len(storage) = index 2
_

これにより、要素へのアクセスが非常に高速になります。

hash(n) % len(storage)hash(m) % len(storage)は同じ数になる可能性があるため、ハッシュはストーリーの大部分にすぎません。その場合、いくつかの異なる戦略で競合を解決できます。 CPythonは複雑なことを行う前に9回「リニアプローブ」を使用するため、他の場所を見る前にスロットの左側を最大9箇所探します。

CPythonのハッシュセットは次のように保存されます。

  • ハッシュセットは、2/3以下のフルにできます。 20個の要素があり、バッキング配列の長さが30個の要素である場合、バッキングストアはサイズが大きくなります。これは、小さなバッキングストアとの衝突が頻繁に発生し、衝突によりすべてが遅くなるためです。

  • バッキングストアは、8から始まる4の累乗でサイズ変更されます。ただし、2の累乗でサイズ変更される大きなセット(50k要素)を除きます:(8、32、128、...)。

したがって、配列を作成すると、バッキングストアの長さは8になります。5がいっぱいになり、要素を追加すると、6つの要素が一時的に含まれます。 _6 > ²⁄₃·8_これにより、サイズ変更がトリガーされ、バッキングストアはサイズ32に4倍になります。

最後に、hash(n)は数値に対してnを返します(特別な_-1_を除く)。


それでは、最初のものを見てみましょう:

_v_set = {88,11,1,33,21,3,7,55,37,8}
_

len(v_set)は10なので、すべてのアイテムが追加された後、バッキングストアは少なくとも15(+1)になります。関連する2のべき乗は32です。したがって、バッキングストアは次のとおりです。

___ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __
_

我々は持っています

_hash(88) % 32 = 24
hash(11) % 32 = 11
hash(1)  % 32 = 1
hash(33) % 32 = 1
hash(21) % 32 = 21
hash(3)  % 32 = 3
hash(7)  % 32 = 7
hash(55) % 32 = 23
hash(37) % 32 = 5
hash(8)  % 32 = 8
_

これらは次のように挿入します:

___  1 __  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __
   33 ← Can't also be where 1 is;
        either 1 or 33 has to move
_

したがって、次のような順序が期待されます

_{[1 or 33], 3, 37, 7, 8, 11, 21, 55, 88}
_

他のどこかで開始されていない1または33。これは線形プローブを使用するため、次のいずれかがあります。

_       ↓
__  1 33  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __
_

または

_       ↓
__ 33  1  3 __ 37 __  7  8 __ __ 11 __ __ __ __ __ __ __ __ __ 21 __ 55 88 __ __ __ __ __ __ __
_

33は、1がすでに存在するために置き換えられるものと予想されるかもしれませんが、セットの作成中にサイズ変更が行われるため、実際にはそうではありません。セットが再構築されるたびに、既に追加されたアイテムは事実上再配列されます。

今、あなたは理由を見ることができます

_{7,5,11,1,4,13,55,12,2,3,6,20,9,10}
_

順番になっているかもしれません。 14個の要素があるため、バッキングストアは少なくとも21 + 1であり、32を意味します。

___ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __ __
_

最初の13スロットで1〜13ハッシュ。 20はスロット20に入ります。

___  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ __ __ 20 __ __ __ __ __ __ __ __ __ __ __
_

55は23であるスロットhash(55) % 32に入ります:

___  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ __ __ 20 __ __ 55 __ __ __ __ __ __ __ __
_

代わりに50を選択した場合、予想されます

___  1  2  3  4  5  6  7  8  9 10 11 12 13 __ __ __ __ 50 __ 20 __ __ __ __ __ __ __ __ __ __ __
_

そして見よ:

_{1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 20, 50}
#>>> {1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 50, 20}
_

popは、見た目によって非常に単純に実装されています。リストを走査して、最初のリストをポップします。


これはすべて実装の詳細です。

36
Veedrac

「任意」は「未決定」と同じものではありません。

彼らが言っているのは、「パブリックインターフェイス」にある辞書の繰り返し順序の有用なプロパティがないということです。現在、辞書の反復を実装するコードによって完全に決定される反復順序のプロパティはほぼ確実に存在しますが、作成者は使用できるものとしてそれらを約束していません。これにより、プログラムが破損することを心配することなく、これらのプロパティをPythonバージョン間(または異なる動作条件で、または実行時に完全にランダムに)で変更できるようになります。

したがって、辞書順のすべてのプロパティに依存するプログラムを記述する場合、辞書タイプを使用する「契約を破る」ことになり、Python開発者は、これをテストすると今のところ機能しているように見えても、これが常に機能することを約束していません。これは、基本的にCの「未定義の動作」に依存することと同等です。

16
Ben

この質問に対する他の回答は、優れており、よく書かれています。 OPは「どのように」と解釈しますが、これを「どうやって逃げるのか」または「なぜ」と解釈します。

Pythonドキュメンテーションは 辞書 は、Python辞書は 抽象データ型 - 連想配列 。彼らが言うように

バインディングが返される順序は任意です

言い換えれば、コンピューターサイエンスの学生は連想配列が順序付けられていると想定することはできません。 math のセットについても同様です

セットの要素がリストされる順序は関係ありません

および コンピューターサイエンス

セットは、特定の順序なしで特定の値を格納できる抽象データ型です

ハッシュテーブルを使用してディクショナリを実装することは、 実装の詳細 です。これは、順序に関する限り連想配列と同じプロパティを持っているという点で興味深いです。

6
John Schmitt

Pythonは、辞書の保存に hash table を使用するため、辞書またはハッシュテーブルを使用するその他の反復可能なオブジェクトには順序がありません。

ただし、ハッシュオブジェクト内のアイテムのインデックスについては、python次のコードに基づいてインデックスを計算します with _hashtable.c_

_key_hash = ht->hash_func(key);
index = key_hash & (ht->num_buckets - 1);
_

そのため、整数のハッシュ値は整数そのものなので* インデックスは数値(_ht->num_buckets - 1_は定数)に基づいているため、インデックスは_(ht->num_buckets - 1)_と数値自体の間のBitwise-andで計算されます* (ハッシュ値が-1の場合は-2になります)、ハッシュ値を持つ他のオブジェクトの場合は。

hash_tableを使用するsetを使用した次の例を検討してください。

_>>> set([0,1919,2000,3,45,33,333,5])
set([0, 33, 3, 5, 45, 333, 2000, 1919])
_

番号_33_の場合:

_33 & (ht->num_buckets - 1) = 1
_

それは実際には:

_'0b100001' & '0b111'= '0b1' # 1 the index of 33
_

この場合__(ht->num_buckets - 1)_は_8-1=7_または_0b111_です。

そして_1919_の場合:

_'0b11101111111' & '0b111' = '0b111' # 7 the index of 1919
_

そして_333_の場合:

_'0b101001101' & '0b111' = '0b101' # 5 the index of 333
_

pythonハッシュ関数の詳細については、 pythonソースコード からの次の引用を読むとよいでしょう。

今後の主な微妙さ:ほとんどのハッシュスキームは、ランダム性をシミュレートするという意味で、「優れた」ハッシュ関数を持つことに依存しています。 Pythonしません:その最も重要なハッシュ関数(文字列および整数用)は、一般的な場合に非常に規則的です:

_>>> map(hash, (0, 1, 2, 3))
  [0, 1, 2, 3]
>>> map(hash, ("namea", "nameb", "namec", "named"))
  [-1658398457, -1658398460, -1658398459, -1658398462]
_

これは必ずしも悪いことではありません!反対に、サイズ2 ** iのテーブルでは、最初のテーブルインデックスとして下位iビットを使用するのが非常に高速であり、連続したintの範囲でインデックス付けされた辞書の衝突はまったくありません。キーが「連続した」文字列である場合、同じことがほぼ当てはまります。したがって、これは一般的な場合にランダムよりも優れた動作を提供し、それは非常に望ましいことです。

OTOH、衝突が発生すると、ハッシュテーブルの連続スライスを埋める傾向があるため、適切な衝突解決戦略が重要になります。ハッシュコードの最後のiビットのみを取得することも脆弱です。たとえば、リスト[i << 16 for i in range(20000)]をキーのセットとして考えてください。 intは独自のハッシュコードであり、これはサイズ2 ** 15の辞書に収まるため、すべてのハッシュコードの最後の15ビットはすべて0です: allは同じテーブルインデックスにマップします。

しかし、異常なケースに対応することで通常のケースが遅くなることはないので、とにかく最後のiビットを使用します。残りを行うのは衝突の解決次第です。通常、最初の試行で探しているキーが見つかった場合(そして、判明すると、通常は実行されます-テーブルの負荷係数は2/3未満に保たれ、したがって、オッズは堅実に有利です)、最初のインデックス計算の汚れを安く保つことは最も理にかなっています。


*クラスintのハッシュ関数:

_class int:
    def __hash__(self):
        value = self
        if value == -1:
            value = -2
        return value
_
5
Kasrâmvd

Python 3.7(および CPython 3.6で既に )、辞書項目 挿入された順序のまま )で始まります。

1
Boris