जब मैं इस पर पुनरावृत्ति कर रहा हूँ और इसे एक सेट से हटा रहा हूँ, तो मुझे यह कई पुनरावृत्तियाँ क्यों मिल रही हैं?


61

पायथन फॉर-लूप को समझने की कोशिश कर रहा है, मैंने सोचा कि यह {1}एक पुनरावृत्ति का परिणाम देगा , या बस एक अनंत लूप में फंस जाएगा, यह निर्भर करता है कि क्या यह सी या अन्य भाषाओं की तरह पुनरावृत्ति करता है। लेकिन वास्तव में ऐसा नहीं था।

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

यह 16 पुनरावृत्तियों को क्यों करता है? परिणाम कहां {16}से आता है?

यह पायथन 3.8.2 का उपयोग कर रहा था। Pypy पर यह अपेक्षित परिणाम देता है {1}


17
आपके द्वारा जोड़े गए आइटमों के आधार पर, प्रत्येक कॉल s.add(i+1)(और संभवतः कॉल करने के लिए s.remove(i)) सेट के पुनरावृत्ति क्रम को बदल सकता है, यह प्रभावित करता है कि सेट इटरेटर जो लूप के लिए बनाया गया है, आगे देखेगा। जब आप सक्रिय पुनरावृत्ति करते हैं, तो किसी वस्तु को उत्परिवर्तित न करें।
चेपनर

6
मैंने उस पर भी ध्यान दिया t = {16}और फिर t.add(15)कहा कि टी सेट है {16, 15}। मुझे लगता है कि समस्या कहीं न कहीं है।

19
यह एक कार्यान्वयन विवरण है - 16 में 15 से कम हैश है (जो कि @ नोट पर ध्यान दिया गया है), इसलिए 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

जवाबों:


86

जब यह लूप समाप्त हो जाएगा तो पायथन कोई वादा नहीं करता है। पुनरावृत्ति के दौरान एक सेट को संशोधित करने से स्किड तत्व, बार-बार तत्व, और अन्य अजीबता हो सकती है। ऐसे व्यवहार पर कभी भरोसा न करें।

मैं जो कुछ भी कहने जा रहा हूं वह कार्यान्वयन विवरण है, बिना सूचना के परिवर्तन के अधीन। यदि आप ऐसा कोई प्रोग्राम लिखते हैं, जो उसमें से किसी पर निर्भर करता है, तो आपका प्रोग्राम पायथन कार्यान्वयन और सीपीथॉन 3.8.2 के अलावा किसी अन्य संस्करण के संयोजन पर टूट सकता है।

लूप 16 पर समाप्त क्यों होता है, इसके लिए संक्षिप्त विवरण यह है कि 16 पहला तत्व है जो पिछले तत्व की तुलना में कम हैश टेबल इंडेक्स में रखा जाता है। पूरा विवरण नीचे है।


पायथन सेट की आंतरिक हैश तालिका में हमेशा 2 आकार की शक्ति होती है। आकार 2 ^ n की तालिका के लिए, यदि कोई टक्कर नहीं होती है, तो तत्व हैश तालिका में स्थिति में उनके हैश के कम से कम महत्वपूर्ण बिट्स के अनुरूप संग्रहीत होते हैं। आप इसे इसमें लागू देख सकते हैं set_add_entry:

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

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

अधिकांश छोटे अजगर अपने आप को हैश; विशेष रूप से, आपके टेस्ट हैश में सभी स्याही खुद को। आप इसे कार्यान्वित देख सकते हैं long_hash। चूँकि आपके सेट में कभी भी दो तत्व नहीं होते हैं, जिनके बराबर हैश में कम बिट्स होते हैं, कोई टक्कर नहीं होती है।


एक पायथन सेट इटेटर एक सेट में अपनी स्थिति का ट्रैक सेट के आंतरिक हैश तालिका में एक साधारण पूर्णांक सूचकांक के साथ रखता है। जब अगले तत्व का अनुरोध किया जाता है, तो इट्रेटर उस सूचकांक पर शुरू होने वाली हैश तालिका में एक आबादी वाली प्रविष्टि की खोज करता है, फिर पाया प्रविष्टि के तुरंत बाद अपने संग्रहीत सूचकांक को सेट करता है और प्रविष्टि के तत्व को वापस करता है। आप इसमें देख सकते हैं 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 पर एक इंट ऑब्जेक्ट के लिए एक संकेतक । Iterator भी अनुक्रमणिका 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_resize8 को इसके दूसरे तर्क के रूप में प्राप्त किया जाता है। इस आधार पर, 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 नहीं है। 16 के 4 सबसे कम बिट्स 0000 हैं, 16 को इंडेक्स में डालते हैं। 0. इटरेटर का स्टोर किया हुआ इंडेक्स इस बिंदु पर 16 है, और जब लूप पुनरावृत्त से अगले तत्व के लिए पूछता है, तो इटरेटर देखता है कि यह पिछले छोर पर चला गया है हैश टेबल।

16सेट में केवल छोड़कर, इट्रेटर इस बिंदु पर लूप को समाप्त करता है ।


14

मेरा मानना ​​है कि यह अजगर में सेट के वास्तविक कार्यान्वयन के साथ कुछ करने के लिए मिला है। सेट अपने आइटम संग्रहीत करने के लिए हैश तालिकाओं का उपयोग करते हैं और इसलिए एक सेट पर पुनरावृत्ति करने का मतलब है कि इसकी हैश तालिका की पंक्तियों पर पुनरावृति।

जैसा कि आप अपने सेट में पुनरावृति करते हैं और आइटम जोड़ते हैं, नए हैश बनाए जा रहे हैं और हैश तालिका में जोड़ा जाता है जब तक आप 16 नंबर तक नहीं पहुंच जाते। इस बिंदु पर, अगला नंबर वास्तव में हैश तालिका की शुरुआत में जोड़ा जाता है और अंत तक नहीं। और जब से आप पहले से ही तालिका की पहली पंक्ति पर पुनरावृत्त हो गए हैं, तब से पुनरावृत्ति लूप समाप्त हो जाता है।

मेरा जवाब पर आधारित है यह एक ऐसी ही सवाल में से एक है, यह वास्तव में इस में ठीक उसी उदाहरण दिखाता है। मैं वास्तव में इसे और अधिक विस्तार से पढ़ने की सलाह देता हूं।


5

अजगर 3 प्रलेखन से:

कोड जो एक संग्रह को संशोधित करता है जबकि उसी संग्रह पर पुनरावृत्ति करना सही पाने के लिए मुश्किल हो सकता है। इसके बजाय, यह आमतौर पर संग्रह की एक प्रति पर लूप करने या नया संग्रह बनाने के लिए अधिक सीधा होता है:

एक कॉपी पर Iterate करें

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]क्योंकि सूचियों का आदेश दिया गया है इसलिए लूप के लिए सूचकांक 0 से शुरू होगा और फिर अगले सूचकांक पर आगे बढ़ेगा, यह पता लगाएगा कि कोई नहीं है, और पाश से बाहर निकलना।


हाँ। लेकिन मेरा सवाल यह है कि यह 16 पुनरावृत्तियों को क्यों बनाता है।
noob अतिप्रवाह

सेट अनियंत्रित है। शब्दकोश और गैर-यादृच्छिक क्रम में पुनरावृति सेट करता है, और पुनरावृति करने के लिए यह एल्गोरिथ्म केवल तभी रखता है जब आप कुछ भी संशोधित नहीं करते हैं। सूचियों और टुपल्स के लिए, यह केवल सूचकांक द्वारा पुनरावृति कर सकता है। जब मैंने आपके कोड को 3.7.2 में आज़माया, तो इसने 8 पुनरावृत्तियों को बनाया।
एरिक जिन

चलना आदेश शायद हैशिंग के साथ करना है, जैसा कि अन्य ने उल्लेख किया है
एरिक जिन

1
इसका क्या मतलब है "किसी प्रकार की स्टैक ट्रेस सॉर्ट चीज़ के कारण"? कोड क्रैश या त्रुटि नहीं करता था इसलिए मैंने कोई स्टैक ट्रेस नहीं देखा था। मैं अजगर में स्टैक ट्रेस कैसे सक्षम करूं?
noob ओवरफ्लो

1

पायथन ने एक अनियोजित संग्रह सेट किया जो तत्व स्थिति या सम्मिलन के आदेश को रिकॉर्ड नहीं करता है। एक अजगर सेट में किसी भी तत्व से जुड़ा कोई सूचकांक नहीं है। इसलिए वे किसी भी अनुक्रमण या स्लाइसिंग ऑपरेशन का समर्थन नहीं करते हैं।

इसलिए उम्मीद न करें कि आपके लिए लूप परिभाषित क्रम में काम करेगा।

यह 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पहले से ही लूप से बाहर निकल गया है।

दिलचस्प तथ्य: 6 और 7 को छोड़कर 16 से कम किसी भी मूल्य का उपयोग हमेशा आपको 16 परिणाम देगा।


"16 से कम किसी भी मूल्य का उपयोग हमेशा आपको 16 परिणाम देगा।" - इसे 6 या 7 के साथ आज़माएं, और आप देखेंगे कि ऐसा नहीं है।
user2357112

@ user2357112 ने मोनिका I को अपडेट किया। धन्यवाद
एकलव्य
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.