2025, Sep 24 11:31
क्या एक ही रन में बिना बदले Python set का iteration order स्थिर रहता है?
जानें कि Python में बिना बदले set का iteration order एक ही रन में स्थिर क्यों रहता है, रन-दर-रन बदल भी सकता है, और PYTHONHASHSEED से reproducible क्रम कैसे पाएं
Python के set बदलने योग्य (mutable) होते हैं और उनकी कोई निश्चित क्रमबद्धता नहीं होती। यह बात सबको पता है, लेकिन अक्सर एक व्यावहारिक प्रश्न उठता है: अगर आप एक ही प्रोग्राम रन के दौरान किसी set पर दो बार इटेरेट करें और बीच में उसे बदलें नहीं, तो क्या आंतरिक rehashing या किसी allocator की "जादुई" हरकत से उसका क्रम बदल सकता है? दूसरे शब्दों में, क्या बिना बदले हुए set का iteration order एक ही execution में स्थिर रहता है?
चिंता को पुन: प्रस्तुत करना
नीचे दिया गया स्निपेट इसी बात को परखता है। एक set बनाया जाता है, पहली पास का क्रम रिकॉर्ड किया जाता है, और बाद में देखा जाता है कि दूसरी पास में वही अनुक्रम मिलता है या नहीं।
bag = {1, 4, 3, 5}
snapshot = [x for x in bag]
# ऐसा अन्य लॉजिक जो set को नहीं छूता
assert [x for x in bag] == snapshot
वास्तव में क्या होता है
Python ग्लॉसरी के अनुसार, किसी hashable ऑब्जेक्ट का hash मान उसकी पूरी आयु में नहीं बदलता। स्टैंडर्ड लाइब्रेरी के दस्तावेज़ बताते हैं कि set में रखे तत्व hashable होने चाहिए। इन दोनों तथ्यों को साथ देखें तो तस्वीर साफ हो जाती है: set का iteration उसके हैशों से बनी हैश-टेबल की संरचना पर निर्भर होता है, और उन हैश मानों में ऑब्जेक्ट के जीवनकाल में बदलाव नहीं होता। नतीजा यह कि जिसे बदला नहीं गया ऐसा set एक ही रन के दौरान इंटरप्रेटर द्वारा न तो अंदरूनी तौर पर rehash होगा, न ही पुन: आवंटित (reallocated) किया जाएगा, और उसका iteration order उसी execution में स्थिर रहेगा।
सूचियाँ (lists) जोड़ने के क्रम को सुरक्षित रखती हैं। जब आप set को list में बदलते हैं, तो सूची उस समय set द्वारा दिए गए क्रम को दिखाती है; और बाद में—जब तक set अपरिवर्तित है—उसी रूपांतरण को दोहराने पर उसी रन में वही सूची क्रम मिलेगा।
व्यवहार में समाधान
यदि आपके कोड को एक ही रन के भीतर set के iteration order का स्थिर स्नैपशॉट चाहिए, तो उसे एक बार ले लें और उसी को दोबारा उपयोग करें। नीचे दिया गया कथन तब तक सही रहेगा जब तक set में कोई बदलाव नहीं किया गया है।
pool = {1, 4, 3, 5}
first_pass = list(pool)
# ऐसा असंबंधित काम जो `pool` में बदलाव नहीं करता
assert list(pool) == first_pass
अलग-अलग रन में भिन्नता के बारे में
अलग-अलग इंटरप्रेटर executions में Python हैशों को रैंडमाइज़ करता है। यानी वही मान रखने वाले set का iteration order रन दर रन बदल सकता है। इसे आप इस तरह देख सकते हैं: किसी ऑब्जेक्ट के hash के आधार पर फ़िल्टर की गई सूची बनाइए, उसे set में बदलिए, और फिर वापस list बनाइए। सूची set के दिए गए क्रम को सहेजती है, और यह क्रम हैश रैंडमाइज़ेशन तथा हैश collisions के कारण अलग executions में बदल सकता है।
for i in 1 2 3 4; do
  python3.12 -c 'import string as alpha; seq = [f"ba{ch}" for ch in alpha.ascii_lowercase if hash(f"ba{ch}") % 8 == 2]; print(f"{seq}\n{set(seq)}\n{list(set(seq))}\n")'
done
परिणाम
['baa', 'bah', 'bai', 'bam', 'bap']
{'baa', 'bap', 'bah', 'bai', 'bam'}
['baa', 'bap', 'bah', 'bai', 'bam']
['bag', 'bai', 'bal', 'bax']
{'bai', 'bag', 'bal', 'bax'}
['bai', 'bag', 'bal', 'bax']
['bae', 'bai', 'baq', 'bax']
{'baq', 'bae', 'bax', 'bai'}
['baq', 'bae', 'bax', 'bai']
['bae', 'bap', 'bax']
{'bae', 'bap', 'bax'}
['bae', 'bap', 'bax']
यदि आपको अलग-अलग रन में पुनरुत्पादित (reproducible) क्रम चाहिए, तो PYTHONHASHSEED को किसी स्थिर मान पर सेट करके hash randomization को बंद कर सकते हैं। तय seed के साथ, क्रम हर execution में समान रहता है।
for i in 1 2 3 4; do
  PYTHONHASHSEED=1 \
  python3.12 -c 'import string as alpha; seq = [f"ba{ch}" for ch in alpha.ascii_lowercase if hash(f"ba{ch}") % 8 == 2]; print(f"{seq}\n{set(seq)}\n{list(set(seq))}\n")'
done
परिणाम
['bac', 'bah', 'bak', 'baw']
{'bac', 'bak', 'bah', 'baw'}
['bac', 'bak', 'bah', 'baw']
['bac', 'bah', 'bak', 'baw']
{'bac', 'bak', 'bah', 'baw'}
['bac', 'bak', 'bah', 'baw']
['bac', 'bah', 'bak', 'baw']
{'bac', 'bak', 'bah', 'baw'}
['bac', 'bak', 'bah', 'baw']
['bac', 'bah', 'bak', 'baw']
{'bac', 'bak', 'bah', 'baw'}
['bac', 'bak', 'bah', 'baw']
यह क्यों मायने रखता है
सेट्स के iteration व्यवहार को समझना सूक्ष्म बगों से बचाता है। एक ही रन में, यदि set बदला नहीं गया है, तो पहले दिखे क्रम पर भरोसा करना सुरक्षित है। लेकिन अलग-अलग रन में यह क्रम पोर्टेबल नहीं होता, जब तक आप hash randomization बंद न करें। यह अंतर परीक्षण लिखते वक्त, सेट-आधारित डेटा को serialize करते समय, या कई executions के आउटपुट की तुलना करते हुए निर्णायक होता है।
मुख्य बातें
यदि आप एक ही प्रोग्राम execution में किसी set पर बिना बदलाव किए कई बार इटेरेट करते हैं, तो क्रम उस रन के दौरान स्थिर रहता है। सूची (list) रूपांतरण के समय set से मिला क्रम सहेज लेती है, इसलिए एक बार स्नैपशॉट लेकर उसे दोबारा इस्तेमाल करें। अलग executions के बीच अंतर अपेक्षित हैं क्योंकि Python हैशों को रैंडमाइज़ करता है; यदि डिबगिंग या परीक्षण के लिए रन-दर-रन एक जैसा क्रम चाहिए, तो PYTHONHASHSEED को किसी स्थिर मान पर सेट करें.
यह लेख StackOverflow पर प्रश्न (लेखक: SLebedev777) और LMC के उत्तर पर आधारित है।