Pythonは、(もしあれば)このループがいつ終了するかについては何も約束しません。反復中にセットを変更すると、要素のスキップ、要素の繰り返し、およびその他の奇妙さが発生する可能性があります。そのような振る舞いに決して頼らないでください。
私が言おうとしていることはすべて実装の詳細であり、予告なく変更されることがあります。いずれかに依存するプログラムを記述した場合、プログラムは、Python実装とCPython 3.8.2以外のバージョンの任意の組み合わせで機能しなくなる可能性があります。
ループが16で終了する理由の簡単な説明は、16が前の要素よりも低いハッシュテーブルインデックスに偶然に配置される最初の要素であるということです。完全な説明は以下のとおりです。
Pythonセットの内部ハッシュテーブルは常に2のべき乗のサイズを持ちます。サイズ2 ^ nのテーブルの場合、衝突が発生しなければ、要素はハッシュテーブルの、ハッシュの最下位ビットnに対応する位置に格納されます。あなたはこれが実装されているのを見ることができますset_add_entry
:
mask = so->mask;
i = (size_t)hash & mask;
entry = &so->table[i];
if (entry->key == NULL)
goto found_unused;
ほとんどの小さなPython intはそれ自体にハッシュします。特に、テスト内のすべてのintはそれ自体にハッシュされます。これはで実装されていlong_hash
ます。セットには、ハッシュに等しい下位ビットを持つ2つの要素が含まれないため、衝突は発生しません。
Pythonセットイテレータは、セットの内部ハッシュテーブルへの単純な整数インデックスを使用して、セット内の位置を追跡します。イテレーターは、次の要素が要求されると、そのインデックスから始まるハッシュテーブル内の入力済みのエントリを検索し、格納されているインデックスを見つかったエントリの直後に設定して、エントリの要素を返します。あなたはこれを見ることができますsetiter_iternext
:
while (i <= mask && (entry[i].key == NULL || entry[i].key == dummy))
i++;
si->si_pos = i+1;
if (i > mask)
goto fail;
si->len--;
key = entry[i].key;
Py_INCREF(key);
return key;
セットは最初、サイズ8のハッシュテーブルと、ハッシュテーブルの0
インデックス0にあるintオブジェクトへのポインターで始まります。イテレータもインデックス0に配置されます。反復処理を行うと、要素がハッシュテーブルに追加されます。要素は次のインデックスに追加されます。これは、ハッシュによって要素が配置されるためです。これが常にイテレータが参照する次のインデックスです。削除された要素には、衝突解決のために、古い位置にダミーマーカーが保存されています。で実装されていることがわかりますset_discard_entry
:
entry = set_lookkey(so, key, hash);
if (entry == NULL)
return -1;
if (entry->key == NULL)
return DISCARD_NOTFOUND;
old_key = entry->key;
entry->key = dummy;
entry->hash = -1;
so->used--;
Py_DECREF(old_key);
return DISCARD_FOUND;
場合4
セットに追加され、セットの要素とダミーの数は、その十分に高いなるset_add_entry
呼び出し、トリガーがハッシュテーブルの再構築しますset_table_resize
。
if ((size_t)so->fill*5 < mask*3)
return 0;
return set_table_resize(so, so->used>50000 ? so->used*2 : so->used*4);
so->used
ハッシュテーブル内のデータが入力されたダミーでないエントリの数です。これは2であるためset_table_resize
、2番目の引数として8を受け取ります。これに基づいて、新しいハッシュテーブルのサイズを16にする必要があるとset_table_resize
判断します。
/* Find the smallest table size > minused. */
/* XXX speed-up with intrinsics */
size_t newsize = PySet_MINSIZE;
while (newsize <= (size_t)minused) {
newsize <<= 1; // The largest possible value is PY_SSIZE_T_MAX + 1.
}
サイズ16のハッシュテーブルを再構築します。すべての要素は、ハッシュに上位ビットが設定されていないため、新しいハッシュテーブルの古いインデックスのままになります。
ループが続くと、要素はイテレータが参照する次のインデックスに配置され続けます。別のハッシュテーブルの再構築がトリガーされますが、新しいサイズは16のままです。
ループが要素として16を追加すると、パターンが壊れます。新しい要素を配置するインデックス16はありません。16の最下位4ビットは0000で、インデックス0に16を置きます。この時点でイテレータの保存されたインデックスは16であり、ループがイテレータから次の要素を要求すると、イテレータはそれがハッシュ表。
イテレータはこの時点でループを終了16
し、セットにのみ残します。
s.add(i+1)
(および場合によってはへの呼び出しs.remove(i)
)は、セットの反復順序を変更し、forループが作成したセットイテレータが次に何を参照するかに影響を与える可能性があります。イテレータがアクティブな間は、オブジェクトを変更しないでください。