Üzerine bir set eklerken ve setten çıkarırken neden bu kadar yineleme alıyorum?


61

Python for-loop'u anlamaya çalışırken, bunun {1}tek bir yineleme için sonuç vereceğini veya C veya diğer dillerde olduğu gibi yinelemeyi yapmasına bağlı olarak sonsuz bir döngüde sıkışacağını düşündüm . Ama aslında hiçbiri olmadı.

>>> s = {0}
>>> for i in s:
...     s.add(i + 1)
...     s.remove(i)
...
>>> print(s)
{16}

Neden 16 yineleme yapıyor? Sonuç {16}nereden geliyor?

Bu Python 3.8.2 kullanıyordu. Pypy'de beklenen sonucu verir {1}.


17
Eklediğiniz öğelere bağlı olarak, yapılan her çağrı s.add(i+1)(ve muhtemelen çağrı s.remove(i)), kümenin yineleme sırasını değiştirerek for döngüsünün oluşturduğu set yineleyicisinin bir sonraki göreceğini etkileyebilir. Etkin bir yineleyiciniz varken nesneyi değiştirmeyin.
chepner

6
Bunu da fark ettim t = {16}ve sonra t.add(15)t'nin {16, 15} kümesi olduğunu veririm . Bence sorun orada bir yerlerde.

19
Bu bir uygulama detayıdır - 16, 15'ten daha düşük bir karmaya sahiptir (@Anon'un fark ettiği şey budur), bu yüzden set türüne 16 ekleyerek yineleyicinin "zaten görülmüş" kısmına eklendi ve böylece yineleyici tükendi.
Błotosmętek

1
Tekneler de okursanız, döngü sırasında yineleyicilerin mutasyona uğratılmasının bazı hatalar yaratabileceğini söyleyen bir not vardır. Bakınız: docs.python.org/3.7/reference/…
Marcello Fabrizio

3
@ Błotosmętek: CPython 3.8.2'de karma (16) == 16 ve karma (15) == 15. Davranış karma değerin kendisinden düşük değildir; öğeler bir kümede doğrudan karma düzeninde saklanmaz.
user2357112 Monica

Yanıtlar:


86

Python, bu döngünün ne zaman (eğer varsa) biteceği konusunda hiçbir söz vermez. Bir kümeyi yineleme sırasında değiştirmek, atlanan öğelere, tekrarlanan öğelere ve diğer tuhaflıklara yol açabilir. Asla böyle davranışlara güvenmeyin.

Söylemek istediğim her şey, önceden haber verilmeksizin değiştirilebilir, uygulama detaylarıdır. Herhangi birine dayanan bir program yazarsanız, programınız Python uygulaması ve CPython 3.8.2 dışındaki sürümlerin herhangi bir kombinasyonunu bozabilir.

Döngünün 16'da neden sona erdiğine dair kısa açıklama, 16'nın önceki öğeden daha düşük bir karma tablo dizinine yerleştirilen ilk öğe olmasıdır. Tam açıklama aşağıdadır.


Bir Python setinin dahili hash tablosu her zaman 2 boyluk bir güce sahiptir. 2 ^ n büyüklüğünde bir tablo için, herhangi bir çarpışma meydana gelmezse, elemanlar, karma tablosundaki, karma değerlerinin en az önemli bitlerine karşılık gelen konumda saklanır. Bunun uygulandığını görebilirsiniz set_add_entry:

mask = so->mask;
i = (size_t)hash & mask;

entry = &so->table[i];
if (entry->key == NULL)
    goto found_unused;

Çoğu küçük Python ints hash eder; özellikle, test karmasındaki tüm ints kendileri için. Bunun uygulandığını görebilirsiniz long_hash. Kümeniz asla karmalarında eşit düşük bitli iki öğe içermediğinden, çarpışma olmaz.


Bir Python seti yineleyicisi, basit bir tamsayı indeksine sahip bir kümedeki konumunu kümenin dahili karma tablosuna kaydeder. Sonraki öğe istendiğinde, yineleyici, bu dizinden başlayarak karma tablosundaki doldurulmuş bir girdiyi arar, ardından depolanan dizinini bulunan girdinin hemen sonrasına ayarlar ve girdinin öğesini döndürür. Bunu şurada görebilirsiniz 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;

Kümeniz başlangıçta 8 büyüklüğünde bir karma tablosu ve karma tablosundaki 00 dizinindeki int nesnesine bir işaretçi ile başlar . Yineleyici de indeks 0'a yerleştirilir. Yinelediğinizde, her biri bir sonraki dizine hash tablosuna öğeler eklenir, çünkü bunların hash'i koymak için söylediği yer budur ve her zaman yineleyicinin baktığı sonraki endeks budur. Kaldırılan elemanlar, çarpışma çözünürlüğü amacıyla eski konumlarında saklanan bir kukla işaretleyiciye sahiptir. Bunun uygulandığını görebilirsiniz 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;

Kümeye 4eklendiğinde, kümedeki öğelerin ve mankenlerin sayısı, set_add_entrybir hash tablosunun yeniden oluşturulmasını tetikleyecek kadar yüksek olur ve şunu çağırır 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->usedhash tablosundaki 2 olan kalabalık, kukla olmayan girişlerin sayısıdır, bu nedenle set_table_resizeikinci argüman olarak 8 alır. Buna dayanarak , yeni karma tablo boyutunun 16 olması gerektiğine set_table_resize karar verir :

/* 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 numaralı karma tabloyu yeniden oluşturur. Tüm öğeler hala yeni karma tablosundaki eski dizinlerinde bulunur, çünkü karma değerlerinde yüksek bitler ayarlanmamıştır.

Döngü devam ettikçe, öğeler yineleyicinin bakacağı bir sonraki dizine yerleştirilmeye devam eder. Başka bir karma tablo yeniden oluşturma tetiklenir, ancak yeni boyut hala 16'dır.

Döngü öğe olarak 16 eklediğinde desen kırılır. Yeni öğenin yerleştirileceği dizin 16 yoktur. 16'nın en düşük 4 biti 0000'dır ve dizin 0'a 16 koyar. Yineleyicinin depolanan dizini bu noktada 16'dır ve döngü yineleyiciden sonraki öğeyi istediğinde, yineleyici bunun sonuna kadar gittiğini görür. karma tablo.

Yineleyici bu noktada döngüyü sonlandırır ve yalnızca 16kümede kalır.


14

Bunun python'daki setlerin gerçek uygulamasıyla bir ilgisi olduğuna inanıyorum. Kümeler öğelerini saklamak için karma tablolar kullanır ve böylece bir kümenin üzerinde yineleme yapmak, karma tablosunun satırları üzerinde yineleme anlamına gelir.

Yinelediğiniz ve setinize öğe ekledikçe, 16 numaraya ulaşana kadar yeni karmalar oluşturulur ve karma tabloya eklenir. Bu noktada, bir sonraki sayı aslında karma tablonun başına eklenir, sonuna değil. Tablonun ilk satırı üzerinde zaten yinelediğiniz için yineleme döngüsü sona erer.

Benim cevabım dayanan bu benzer bir soru birinde, aslında bu tam aynı örneği göstermektedir. Gerçekten daha fazla detay için okumanızı tavsiye ederim.


5

Python 3 belgelerinden:

Aynı koleksiyon üzerinde yineleme yaparken bir koleksiyonu değiştiren kod, doğru elde etmek zor olabilir. Bunun yerine, koleksiyonun bir kopyası üzerinde döngü yapmak veya yeni bir koleksiyon oluşturmak genellikle daha basittir:

Bir kopya üzerinde yineleme

s = {0}
s2 = s.copy()
for i in s2:
     s.add(i + 1)
     s.remove(i)

sadece 1 kez tekrarlamalı

>>> print(s)
{1}
>>> print(s2)
{0}

Düzenleme: Bu yineleme için olası bir nedeni, bir küme sırasız olduğundan, bir yığın yığın izleme tür şey neden olmasıdır. Bunu bir kümeyle değil bir liste ile yaparsanız, s = [1]listeler sıralandığından, for döngüsünün dizin 0 ile başlayıp bir sonraki dizine geçerek, bir tane olmadığını bulma ve döngüden çıkılıyor.


Evet. Ama sorum şu ki, neden 16 iterasyon yapıyor.
çaylak taşması

set sıralanmamış. Sözlükler ve yinelemeleri rasgele olmayan bir sırada yineler ve yineleme için bu algoritma yalnızca hiçbir şeyi değiştirmezseniz tutar. Listeler ve tuples için, sadece dizine göre yineleyebilir. Kodunuzu 3.7.2'de denediğimde 8 tekrar yaptı.
Eric Jin

Yineleme sırası muhtemelen diğerlerinin belirttiği gibi karma ile ilgilidir
Eric Jin

1
"Bir yığın yığın izlemesine neden olmak" ne anlama geliyor? Kod çökme veya hata yapmadı, bu yüzden herhangi bir yığın izleme görmedim. Python'da yığın izlemeyi nasıl etkinleştiririm?
çaylak taşması

1

Python öğenin konumunu veya ekleme sırasını kaydetmeyen sıralanmamış bir koleksiyon ayarladı. Python kümesindeki hiçbir öğeye bağlı dizin yok. Bu nedenle herhangi bir indeksleme veya dilimleme işlemini desteklemezler.

Bu yüzden for döngünüzün tanımlı bir sırada çalışmasını beklemeyin.

Neden 16 yineleme yapıyor?

user2357112 supports Monicaana nedeni zaten açıklıyor. Burada başka bir düşünce tarzı var.

s = {0}
for i in s:
     s.add(i + 1)
     print(s)
     s.remove(i)
print(s)

Bu kodu çalıştırdığınızda size şu çıktıyı verir:

{0, 1}                                                                                                                               
{1, 2}                                                                                                                               
{2, 3}                                                                                                                               
{3, 4}                                                                                                                               
{4, 5}                                                                                                                               
{5, 6}                                                                                                                               
{6, 7}                                                                                                                               
{7, 8}
{8, 9}                                                                                                                               
{9, 10}                                                                                                                              
{10, 11}                                                                                                                             
{11, 12}                                                                                                                             
{12, 13}                                                                                                                             
{13, 14}                                                                                                                             
{14, 15}                                                                                                                             
{16, 15}                                                                                                                             
{16}       

Döngü veya setin yazdırılması gibi tüm öğelere birlikte eriştiğimizde, setin tamamını geçmesi için önceden tanımlanmış bir sipariş olmalıdır. Yani, son tekrarında sipariş dan gibi değiştirilir göreceksiniz {i,i+1}için {i+1,i}.

Son yinelemeden sonra, i+1döngüden çıkış için zaten geçilmiş oldu .

İlginç Gerçek: 6 ve 7 dışında 16'dan küçük herhangi bir değer kullanın, her zaman 16 sonucunu verir.


"16'dan küçük bir değer kullanmanız her zaman 16 sonucunu verecektir." - 6 veya 7 ile deneyin ve görmeyeceğinizi göreceksiniz.
user2357112 Monica

@ user2357112 destekliyor Monica Ben güncelledim. Thanks
Eklavya
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.