ทำไมฉันถึงได้รับการทำซ้ำหลายครั้งเมื่อเพิ่มและลบออกจากชุดในขณะที่วนซ้ำ


61

พยายามที่จะเข้าใจ Python for-loop ฉันคิดว่านี่จะให้ผลลัพธ์{1}สำหรับการวนซ้ำหนึ่งครั้งหรือแค่ติดอยู่ในการวนซ้ำไม่สิ้นสุดขึ้นอยู่กับว่ามันทำซ้ำใน C หรือภาษาอื่น ๆ หรือไม่ แต่จริงๆแล้วมันไม่ได้ทำ

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

ทำไมถึงทำซ้ำ 16 ครั้ง? ผลลัพธ์อยู่ที่ไหน{16}มาจากไหน

นี่คือการใช้ Python 3.8.2 เมื่อวันที่ pypy {1}มันทำให้ผลที่คาดหวัง


17
ขึ้นอยู่กับรายการที่คุณเพิ่มแต่ละการเรียกไปที่s.add(i+1)(และอาจเป็นไปได้ที่การเรียกs.remove(i)) สามารถเปลี่ยนลำดับการวนซ้ำของชุดได้ อย่ากลายพันธุ์วัตถุในขณะที่คุณมีตัววนซ้ำที่ใช้งานอยู่
chepner

6
ฉันสังเกตเห็นเช่นt = {16}นั้นแล้วt.add(15)ให้ผลว่า t คือเซต {16, 15} ฉันคิดว่าปัญหาอยู่ที่นั่นที่ไหนซักแห่ง

19
มันเป็นรายละเอียดการนำไปใช้งาน - 16 มีแฮชที่ต่ำกว่า 15 (นั่นคือสิ่งที่ @Anon สังเกตเห็น) ดังนั้นการเพิ่ม 16 ลงในประเภทชุดที่เพิ่มเข้าไปในส่วน "เห็นแล้ว" ของตัววนซ้ำและตัววนซ้ำหมดแล้ว
Błotosmętek

1
หากคุณอ่านเอกสารที่มีข้อความระบุว่าการทำซ้ำตัววนซ้ำระหว่างการวนซ้ำอาจสร้างข้อบกพร่องบางอย่าง ดู: docs.python.org/3.7/reference/…
Marcello Fabrizio

3
@ Błotosmętek: บน CPython 3.8.2, แฮช (16) == 16 และแฮช (15) == 15. พฤติกรรมไม่ได้มาจากแฮชที่ลดลง องค์ประกอบจะไม่ถูกจัดเก็บโดยตรงในลำดับแฮชในชุด
user2357112 รองรับ Monica

คำตอบ:


86

Python ไม่รับประกันว่าห่วงนี้จะจบเมื่อใด (ถ้ามี) การแก้ไขชุดระหว่างการทำซ้ำอาจนำไปสู่องค์ประกอบที่ข้ามองค์ประกอบที่ทำซ้ำและความแปลกประหลาดอื่น ๆ อย่าพึ่งพาพฤติกรรมดังกล่าว

ทุกสิ่งที่ฉันจะพูดคือรายละเอียดการใช้งานอาจมีการเปลี่ยนแปลงโดยไม่ต้องแจ้งให้ทราบล่วงหน้า หากคุณเขียนโปรแกรมที่ใช้โปรแกรมใดโปรแกรมหนึ่งของคุณอาจหยุดการทำงานของ Python และรุ่นอื่นที่ไม่ใช่ CPython 3.8.2

คำอธิบายสั้น ๆ ว่าทำไมลูปสิ้นสุดที่ 16 คือ 16 คือองค์ประกอบแรกที่เกิดขึ้นที่ดัชนีตารางแฮชต่ำกว่าองค์ประกอบก่อนหน้า คำอธิบายแบบเต็มอยู่ด้านล่าง


ตารางแฮชภายในของชุด Python มีขนาดกำลังสองเสมอ สำหรับตารางขนาด 2 ^ n หากไม่มีการชนเกิดขึ้นองค์ประกอบต่างๆจะถูกเก็บไว้ในตำแหน่งในตารางแฮชที่สอดคล้องกับบิตแฮชที่มีนัยสำคัญน้อยที่สุด คุณสามารถเห็นสิ่งนี้นำมาใช้ในset_add_entry:

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

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

Python ขนาดเล็กส่วนใหญ่มีการแฮชกับตัวเอง โดยเฉพาะอย่างยิ่ง ints ทั้งหมดในการทดสอบแฮชของตัวเอง long_hashคุณสามารถดูนี้ดำเนินการใน เนื่องจากชุดของคุณไม่เคยมีสององค์ประกอบที่มีบิตต่ำเท่ากันในแฮชของพวกเขาจึงไม่เกิดการชนกัน


Python ชุด iterator ติดตามตำแหน่งในชุดที่มีดัชนีจำนวนเต็มอย่างง่ายเข้าไปในตารางแฮชภายในของชุด เมื่อมีการร้องขอองค์ประกอบถัดไปตัววนซ้ำจะค้นหารายการที่มีประชากรในตารางแฮชเริ่มต้นที่ดัชนีนั้นจากนั้นตั้งค่าดัชนีที่เก็บไว้เป็นทันทีหลังจากรายการที่พบและส่งกลับองค์ประกอบของรายการ คุณสามารถเห็นสิ่งนี้ใน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วัตถุ int ที่ดัชนี 0 ในตารางแฮช ตัววนซ้ำยังอยู่ในตำแหน่งที่ดัชนี 0 เมื่อคุณวนซ้ำองค์ประกอบจะถูกเพิ่มลงในตารางแฮชแต่ละรายการในดัชนีถัดไปเพราะนั่นคือที่ที่แฮชของพวกเขาบอกว่าจะใส่มันและนั่นคือดัชนีถัดไปที่ iterator มอง องค์ประกอบที่ถูกลบออกจะมีเครื่องหมายหุ่นจำลองเก็บไว้ที่ตำแหน่งเดิมเพื่อการแก้ไขปัญหาการชน คุณจะเห็นว่ามีการใช้งานใน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รับ 8 เป็นอาร์กิวเมนต์ที่สอง ขึ้นอยู่กับสิ่งนี้set_table_resize ตัดสินใจว่าขนาดตารางแฮชใหม่ควรเป็น 16:

/* 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 ที่จะวางองค์ประกอบใหม่ที่ 4 บิตต่ำสุดของ 16 คือ 0000 ใส่ 16 ที่ดัชนี 0 ดัชนีที่เก็บไว้ของ iterator คือ 16 ณ จุดนี้และเมื่อลูปขอองค์ประกอบถัดไปจาก iterator iterator เห็นว่ามันได้ผ่านจุดสิ้นสุดของ ตารางแฮช

ตัววนซ้ำสิ้นสุดการวนซ้ำ ณ จุดนี้เหลือเฉพาะ16ในชุดเท่านั้น


14

ฉันเชื่อว่าสิ่งนี้มีบางอย่างที่เกี่ยวข้องกับการใช้งานจริงของชุดในหลาม ชุดใช้ตารางแฮชสำหรับจัดเก็บรายการของพวกเขาดังนั้นการวนซ้ำชุดหมายถึงการวนซ้ำแถวของตารางแฮช

เมื่อคุณวนซ้ำและเพิ่มรายการในชุดของคุณจะมีการสร้างแฮชใหม่และผนวกเข้ากับตารางแฮชจนกว่าคุณจะไปถึงหมายเลข 16 ณ จุดนี้หมายเลขถัดไปจะถูกเพิ่มเข้าไปในตอนต้นของตารางแฮชและไม่สิ้นสุด และเนื่องจากคุณได้ทำการวนซ้ำแถวแรกของตารางแล้วการวนซ้ำจะสิ้นสุดลง

คำตอบของฉันอยู่บนพื้นฐานนี้หนึ่งในคำถามที่คล้ายกันก็จริงแสดงตัวอย่างเดียวกันนี้ ฉันขอแนะนำให้อ่านรายละเอียดเพิ่มเติม


5

จากเอกสาร python 3:

รหัสที่ปรับเปลี่ยนคอลเลกชันในขณะที่วนซ้ำคอลเลกชันเดียวกันนั้นอาจเป็นเรื่องยาก โดยปกติแล้วจะตรงไปข้างหน้ามากกว่าการวนซ้ำสำเนาของคอลเลกชันหรือสร้างคอลเลกชันใหม่:

ทำซ้ำสำเนา

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

ซึ่งควรวนซ้ำเพียง 1 ครั้ง

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

แก้ไข: สาเหตุที่เป็นไปได้สำหรับการทำซ้ำนี้เป็นเพราะชุดไม่มีการเรียงลำดับทำให้เกิดการเรียงลำดับของกองการติดตามบางอย่าง หากคุณทำรายการและไม่ได้ตั้งค่ามันจะจบลงs = [1]เพราะรายการมีการเรียงลำดับดังนั้น for for loop จะเริ่มด้วยดัชนี 0 จากนั้นย้ายไปยังดัชนีถัดไปเพื่อหาว่าไม่มีและ ออกจากลูป


ใช่. แต่คำถามของฉันคือทำไมมันทำซ้ำ 16
noob ล้น

ชุดนี้ไม่มีการเรียงลำดับ พจนานุกรมและตั้งค่าแบบวนซ้ำตามลำดับแบบไม่สุ่มและอัลกอริธึมนี้ในการทำซ้ำจะเก็บเฉพาะเมื่อคุณไม่ได้แก้ไขอะไรเลย สำหรับรายการและสิ่งอันดับมันสามารถทำซ้ำตามดัชนี เมื่อฉันลองใช้รหัสของคุณใน 3.7.2 มันทำซ้ำ 8 ครั้ง
Eric Jin

ลำดับการทำซ้ำอาจเกี่ยวข้องกับการแปลงแป้นพิมพ์ตามที่คนอื่นพูดถึง
เอริคจิน

1
มันหมายความว่าอะไร "ทำให้เกิดการเรียงลำดับการติดตามสแต็ก" รหัสไม่ได้ทำงานผิดพลาดหรือมีข้อผิดพลาดดังนั้นฉันจึงไม่เห็นร่องรอยการติดตามสแต็ก ฉันจะเปิดใช้งานการติดตามสแต็กในหลามได้อย่างไร
noob ล้น

1

Python ตั้งคอลเลคชั่นที่ไม่เรียงลำดับซึ่งไม่ได้บันทึกตำแหน่งองค์ประกอบหรือคำสั่งของการแทรก ไม่มีดัชนีติดอยู่กับองค์ประกอบใด ๆ ในชุดไพ ธ อน ดังนั้นพวกเขาจึงไม่สนับสนุนการทำดัชนีหรือการแบ่งส่วนข้อมูลใด ๆ

ดังนั้นอย่าคาดหวังว่าลูป for ของคุณจะทำงานตามลำดับที่กำหนดไว้

ทำไมถึงทำซ้ำ 16 ครั้ง?

user2357112 supports Monicaอธิบายสาเหตุหลักแล้ว นี่คือวิธีคิดอีกวิธีหนึ่ง

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

เมื่อคุณเรียกใช้รหัสนี้มันจะให้ผลลัพธ์นี้:

{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}       

เมื่อเราเข้าถึงองค์ประกอบทั้งหมดเข้าด้วยกันเช่นวนรอบหรือพิมพ์ชุดจะต้องมีคำสั่งที่กำหนดไว้ล่วงหน้าเพื่อให้สำรวจทั้งชุด ดังนั้นในการทำซ้ำสุดท้ายที่คุณจะเห็นคำสั่งซื้อที่มีการเปลี่ยนแปลงเช่นจากการ{i,i+1}{i+1,i}

หลังจากการทำซ้ำครั้งล่าสุดมันเกิดขึ้นที่i+1มีการสำรวจแล้วเพื่อออกจากวง

ข้อเท็จจริงที่น่าสนใจ: ใช้ค่าใด ๆ ที่น้อยกว่า 16 ยกเว้น 6 และ 7 จะให้ผลลัพธ์ที่ 16 เสมอ


"ใช้ค่าใด ๆ ที่น้อยกว่า 16 จะให้ผลลัพธ์ 16 เสมอ" - ลองด้วย 6 หรือ 7 จากนั้นคุณจะเห็นว่าไม่มี
user2357112 รองรับ Monica

@ user2357112 รองรับ Monica I ที่อัปเดตแล้ว ขอบคุณ
Eklavya
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.