web-dev-qa-db-ja.com

順序なしの「順序」Pythonセット

Pythonのセットは順序付けされていないことを理解していますが、一貫性があるように見えるため、それらが表示される「順序」に興味があります。順序が正しくないようです。毎回同じように:

>>> set_1 = set([5, 2, 7, 2, 1, 88])
>>> set_2 = set([5, 2, 7, 2, 1, 88])
>>> set_1
set([88, 1, 2, 5, 7])
>>> set_2
set([88, 1, 2, 5, 7])

...そして別の例:

>>> set_3 = set('abracadabra')
>>> set_4 = set('abracadabra')
>>> set_3
set(['a', 'r', 'b', 'c', 'd'])
>>>> set_4
set(['a', 'r', 'b', 'c', 'd'])

どうしてこれなのか気になります。何か助けは?

50
ivan

これを見る必要があります ビデオ (CPythonですが)1 辞書の詳細について-しかし、私はそれがセットにも適用されると思います)。

基本的に、pythonは要素をハッシュし、最後のNビット(Nはセットのサイズによって決定されます)を取得し、それらのビットを配列インデックスとして使用してオブジェクトをメモリに配置します。オブジェクトはもちろん、ハッシュ間の衝突を解決する必要がある場合、画像はもう少し複雑になりますが、それが要点です。

また、それらが出力される順序は、(衝突のため)それらを配置する順序によって決定されることに注意してください。そのため、_set_2_に渡すリストを並べ替えると、キーの衝突がある場合に別の順序になる可能性があります。

例えば:

_list1 = [8,16,24]
set(list1)        #set([8, 16, 24])
list2 = [24,16,8]
set(list2)        #set([24, 16, 8])
_

順序がこれらのセットで保持されるという事実は「偶然」であり、衝突解決と関係があります(これについては何も知りません)。重要なのは、hash(8)hash(16)、およびhash(24)の最後の3ビットが同じであることです。それらは同じであるため、衝突解決は最初の(最良の)選択ではなく「バックアップ」メモリロケーションに要素を置き、_8_がロケーションを占有するか__16_がどちらを使用するかによって決定されます。最初にパーティーに到着し、「最高の席」を取った。

_1_、_2_、_3_を使用して例を繰り返すと、入力リストでの順序に関係なく、一貫した順序になります。

_list1 = [1,2,3]
set(list1)      # set([1, 2, 3])
list2 = [3,2,1]
set(list2)      # set([1, 2, 3])
_

hash(1)の最後の3ビット以降、hash(2)およびhash(3)は一意です。


1ここで説明する実装は、CPython dictおよびsetに適用されます。一般的な説明は、3.6までのすべての最新バージョンのCPythonに有効であると思います。ただし、CPython3.6以降では、dictの反復の挿入順序を実際に保持する追加の実装詳細があります。 setにはまだこのプロパティがないようです。データ構造は このブログ投稿 によって記述されています(CPythonの前にこれを使い始めた)pypyの人々。オリジナルのアイデア(少なくともpythonエコシステムの場合) python-devメーリングリストにアーカイブされています

39
mgilson

そのような動作の理由は、Python辞書の実装にハッシュテーブルを使用する: https://en.wikipedia.org/wiki/Hash_table#Open_addressing よりです。

キーの位置は、そのメモリアドレスによって定義されます。知っている場合Python一部のオブジェクトのメモリを再利用します:

>>> a = 'Hello world'
>>> id(a)
140058096568768
>>> a = 'Hello world'
>>> id(a)
140058096568480

オブジェクトaは、初期化されるたびに異なるアドレスを持っていることがわかります。

ただし、小さな整数の場合は変更されません。

>>> a = 1
>>> id(a)
40060856
>>> a = 1
>>> id(a)
40060856

別の名前で2番目のオブジェクトを作成しても、同じになります。

>>> b = 1
>>> id(b)
40060856

このアプローチにより、Pythonインタプリタが消費するメモリを節約できます。

4
Eugene Soldatov

AFAIK Pythonセットは ハッシュテーブル を使用して実装されます。項目が表示される順序は、使用されるハッシュ関数によって異なります。プログラムの同じ実行内で、ハッシュ関数はおそらく変更されないため、同じ順序になります。

ただし、常に同じ関数を使用する保証はなく、順序は実行間で、または同じ実行内で、多くの要素を挿入してハッシュテーブルのサイズを変更する必要がある場合に変更されます。

3
MAK

セットはハッシュテーブルに基づいています。値のハッシュは一貫している必要があるため、順序も同じになります。ただし、2つの要素が同じコードにハッシュしない限り、挿入の順序によって出力順序が変更されます。

2
Mark Ransom

mgilsonの素晴らしい答え が示唆されているが、既存の答えのいずれにも明示的に言及されていない重要な点の1つ:

小さな整数はそれ自身にハッシュします:

>>> [hash(x) for x in (1, 2, 3, 88)]
[1, 2, 3, 88]

文字列は、予測できない値にハッシュされます。実際、3.3以降では、デフォルトで これらは起動時にランダム化されるシードから構築されます 。したがって、新しいPythonインタプリタセッションごとに異なる結果が得られますが、次のようになります。

>>> [hash(x) for x in 'abcz']
[6014072853767888837,
 8680706751544317651,
 -7529624133683586553,
 -1982255696180680242]

したがって、可能な限り単純なハッシュテーブルの実装を検討してください。N要素の配列だけです。値を挿入すると、値がhash(value) % Nに配置されます(衝突がないと仮定)。そして、Nの大きさを大まかに推測できます。これは、その中の要素の数よりも少し大きくなります。 6つの要素のシーケンスからセットを作成する場合、Nは簡単に、たとえば8になります。

これらの5つの数値をN = 8で保存するとどうなりますか?まあ、hash(1) % 8hash(2) % 8などは単なる数値ですが、hash(88) % 8は0です。そのため、ハッシュテーブルの配列は最終的に88, 1, 2, NULL, NULL, 5, NULL, 7を保持します。したがって、セットを繰り返すと88, 1, 2, 5, 7が得られる理由を簡単に理解できるはずです。

もちろんPythonは毎回この注文を受け取ることを保証しません保証しません。方法への小さな変更Nの正しい値を推測すると、88がどこか別の場所に到達する(または他の値のいずれかと衝突する)ことを意味します。実際、私のMacでCPython 3.7を実行すると、1, 2, 5, 7, 88。0

一方、サイズが11のシーケンスからハッシュを作成し、ランダムなハッシュを挿入すると、どうなりますか?最も単純な実装を想定し、衝突がないと想定しても、どのような順序になるのかまだわかりません。 Pythonインタプリタの1回の実行では一貫性がありますが、次回起動するときに異なります(PYTHONHASHSEED0または他のintに設定しない限り)値)これはまさにあなたが見るものです。


もちろん、推測するよりも、 セットが実際に実装される方法 を検討する価値があります。しかし、最も単純なハッシュテーブルの実装の仮定に基づいて推測するのは、正確に何が起こるかです(衝突がなければ、ハッシュテーブルの拡張は禁じられています)。

1
abarnert