辞書とセットの順序が任意であるのはなぜですか?


151

辞書またはPythonで設定されたループのループが「任意」の順序でどのように実行されるのか理解できません。

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

何が欠けていますか?


1
最新のPyPyビルド(2.5、Python 2.7の場合)では、辞書がデフォルトで並べ替えられます
Veedrac、2015

回答:


236

注:この回答はdict、Python 3.6で型の実装が変更される前に作成されました。この回答の実装の詳細のほとんどは引き続き適用されますが、辞書内のキーのリスト順はハッシュ値によって決定されなくなりました。セットの実装は変更されていません。

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

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

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

たとえば、キー'foo'とを'bar'考えてみます。テーブルサイズが8スロットであるとします。Python 2.7では、hash('foo')is -4177197833195190597hash('bar')is 327024216814240868です。つまり、これらの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の実装ですべての(十分に文書化された)詳細を確認してください。また、CPythonの仕組みに関するBrandon RhodesによるこのPycon 2010のプレゼンテーションをご覧になるdictBeautiful Codeのコピーを入手してください。、Andrew Kuchlingによって書かれた実装に関する章を含むます。

Python 3.3以降では、ランダムハッシュシードも使用されるため、特定の種類のサービス拒否(攻撃者が大量のハッシュの衝突を引き起こしてPythonサーバーを応答不能にする)を防ぐために、ハッシュの衝突を予測できなくなります。つまり、特定の辞書またはセットの順序、現在のPython呼び出しのランダムハッシュシードに依存します。

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

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

Python 2.7以降では、キーの順序を記録するための追加のデータ構造を追加するサブクラスであるOrderedDictクラスも提供していますdict。ある程度の速度と追加のメモリを犠牲にして、このクラスはキーを挿入した順序を記憶します。キー、値、またはアイテムをリストすると、その順序でそのようになります。追加のディクショナリに保存されている二重リンクリストを使用して、注文を効率的に最新に保ちます。アイデアを概説するレイモンド・ヘッティンガー投稿をご覧ください。OrderedDictオブジェクトには、並べ替え可能などの他の利点があります

注文セットが必要な場合は、osetパッケージをインストールできます。Python 2.5以降で動作します。


1
私は他のPython実装が何らかの方法でハッシュテーブルではないものを使用できるとは思いません(ハッシュテーブルを実装する方法は数十億あるので、まだある程度の自由があります)。辞書は使用しているという事実__hash____eq__(何もない)は、実質的に言語の保証ではなく、実装の詳細です。

1
@delnan:ハッシュと等価テストでBTreeをまだ使用できるかどうか疑問に思います。:-)
Martijn Pieters

1
それは確かに正しいし、実現可能性について間違っていることが証明されてうれしいだろうが、より広い契約を必要とせずにハッシュテーブルを打ち負かす方法は見当たらない。BTreeの場合、平均ケースのパフォーマンスは向上せず、ワーストケースも改善されません(ハッシュの衝突は依然として線形検索を意味します)。したがって、多くのハッシュの適合性(mod tablesize)に対する抵抗が増すだけであり、それを処理する他の多くの優れた方法(いくつかはで使用されますdictobject.c)があり、BTreeが適切なものを見つけるのに必要な比較よりもはるかに少ない比較になるサブツリー。

@delnan:私は完全に同意します。私は何よりも、他の実装オプションを許可しないことで打ちのめされたくありませんでした。
Martijn Pieters

37

これは、複製としてクローズされる前のPython 3.41 Aセットに対する応答です。


他は正しいです:順序に依存しないでください。そこにあるふりをしないでください。

とはいえ、信頼できる点が1つあります。

list(myset) == list(myset)

つまり、順序は安定しています。


認識された秩序がある理由を理解するには、いくつかのことを理解する必要があります。

  • その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要素の長さである場合、バッキングストアはサイズが大きくなるようにサイズ変更されます。これは、バッキングストアが小さいと衝突が頻繁に発生し、衝突によってすべてが遅くなるためです。

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

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

最後に、数値を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 hash(55) % 32は23のスロットに入ります。

__  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 物事の見た目によって非常に単純に実装されています:リストを走査して最初のリストをポップします。


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


17

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

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

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


3
辞書の反復の一部は明確に定義されていることに注意してください。特定の辞書のキー、値、または項目の反復は、その間に辞書が変更されていない限り、同じ順序で行われます。つまり、d.items()と本質的に同じですzip(d.keys(), d.values())。ただし、ディクショナリにアイテムが追加された場合、すべてのベットはオフになります。順序は完全に変わる可能性があります(ハッシュテーブルのサイズを変更する必要がある場合)。ただし、ほとんどの場合、新しいアイテムがシーケンスの任意の場所に表示されるだけです。
Blckknght 2013年

6

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

Pythonのドキュメントでは、Python辞書は抽象データ型の連想配列を実装しているため、辞書は順序付けされていません。彼らが言うように

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

つまり、コンピュータサイエンスの学生は、連想配列が順序付けられていると想定することはできません。数学の集合にも同じことが言えます

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

コンピュータサイエンス

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

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


1
あなたは基本的に正しいですが、assoc配列ではなくハッシュテーブルの実装であると言う方が少し近い(そして「順序付けされていない」理由についての良いヒントを与える)でしょう。
2ビットの錬金術師

5

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

しかし、ハッシュオブジェクト内のアイテムのインデックスに関して、Pythonは以下のコードに基づいてインデックスを計算しますhashtable.c

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

整数のハッシュ値自体が整数であるように、そのため、*インデックスは数に基づいているが(ht->num_buckets - 1によって算出された指標に定数である)ビット単位-との間で(ht->num_buckets - 1)、それ自体は数*(のため-1のハッシュがある期待-2 )、およびハッシュ値を持つ他のオブジェクトの場合。

sethash-tableを使用する次の例を考えてみます。

>>> 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ビットを初期テーブルインデックスとして使用すると非常に高速であり、連続した範囲の整数によってインデックスが付けられたディクショナリでもまったく衝突がありません。キーが「連続した」文字列の場合もほぼ同じです。したがって、これは一般的なケースでランダムよりも優れた動作を提供し、それは非常に望ましいことです。

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

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


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

class int:
    def __hash__(self):
        value = self
        if value == -1:
            value = -2
        return value


弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.