2025, Oct 18 01:17
Python list slicing असाइनमेंट: दाईं slice पर असल में क्या होता है
Python list slicing असाइनमेंट में दाईं slice का असली व्यवहार जानें: क्या नई सूची बनती है या in-place होता है। CPython विवरण और विकल्प अंदर, उदाहरण सहित।
Python सूची के एक स्लाइस को उसी सूची के किसी दूसरे स्लाइस से फिर असाइन करते समय एक स्वाभाविक सवाल उठता है: क्या दाईं ओर वाला भाग अस्थायी सूची बनाता है, या कोई ऐसी in-place अनुकूलन मौजूद है जो अतिरिक्त आवंटन से बचा दे? असल में क्या होता है, इसे समझना आपको पूर्वानुमेय और कुशल कोड लिखने में मदद करता है और उन अनुकूलनों पर निर्भर रहने से बचाता है जिनकी कोई गारंटी नहीं होती।
समस्या की रूपरेखा
मुख्य ऑपरेशन कुछ यूँ दिखता है:
L[a:b] = L[c:d]Python के ट्यूटोरियल में slicing के बारे में यह कहा गया है:
सभी slice ऑपरेशन माँगे गए तत्वों वाली एक नई list लौटाते हैं।
लेकिन यह स्पष्ट रूप से नहीं बताया गया कि क्या उसी list से लिए गए slice को असाइन करते समय अस्थायी list बनती है या इसे किसी अनुकूलन से टाला जा सकता है।
वह न्यूनतम कोड जो सवाल उठाता है
नीचे दिया गया फ़ंक्शन एक slice पढ़ता है और उसी list के दूसरे slice में उसे लिख देता है:
def demo_fn():
    lst_ref = [1, 2, 3, 4]
    lst_ref[0:1] = lst_ref[2:3]
    return lst_refथोड़ा बदला हुआ संस्करण दाईं ओर को बढ़ाकर बीच का चरण और स्पष्ट कर देता है:
def demo_plus():
    buf = [1, 2, 3, 4]
    buf[0:1] = buf[2:3] + [5, 6]
    return bufअसल में क्या होता है और क्यों
पहले दाईं ओर का भाग मूल्यांकित होता है, और उसी दौरान एक नई list बनती है। यानी बाएँ हिस्से को अपडेट करने से पहले दाईं ओर का slice expression माँगे गए तत्वों वाली एक ताज़ा list लौटाता है। CPython में ऐसे कोड को डिसअसेंबल करने पर इसका प्रमाण दिखता है: slice पढ़ना BINARY_SLICE से होता है और लिखना STORE_SLICE से, और इनके बीच list वास्तविक रूप में बनती है। अलग‑अलग read और write ऑपरेशन का मौजूद होना साफ़ दिखाता है कि list के अंदरूनी हिस्सों पर कोई विशेष 'memmove' जैसा in-place काम नहीं हो रहा।
इसके पीछे दो व्यावहारिक वजहें हैं। पहला, Python सूची के तत्व ऑब्जेक्ट्स के स्वतंत्र संदर्भ (references) होते हैं। उन्हीं ऑब्जेक्ट्स को दूसरी जगह दोबारा रखने पर reference counts को ठीक से अपडेट करना ज़रूरी है; सीधे कच्ची मेमरी कॉपी कर देने से काउंट नहीं बदलेंगे और शुद्धता टूट जाएगी। दूसरा, slice असाइनमेंट सूची की लंबाई बदल सकता है, जिससे तत्वों को शिफ्ट करना पड़ता है। सामान्य slice पुनःअसाइनमेंट के लिए in-place मेमरी मूव जल्दी ही जटिल और त्रुटि‑प्रवण हो जाता है, खासकर जब स्रोत और लक्ष्य ओवरलैप करें।
यह भी अहम है कि सामान्य स्थिति में Python किसी list‑विशिष्ट तरकीब पर भरोसा नहीं कर सकता। बाएँ पक्ष वाला ऑब्जेक्ट built‑in list हो, यह आवश्यक नहीं; प्रकार (types) slicing और assignment को कस्टमाइज़ कर सकते हैं। यह लचीलापन अलग‑अलग ऑब्जेक्ट्स और टाइप्स के बीच विशेष bytecode अनुकूलन को व्यवहार में काफ़ी मुश्किल बना देता है।
व्यावहारिक समाधान और विकल्प
जिस व्यवहार पर भरोसा करना चाहिए, वह सीधा है: दाईं ओर का slice expression एक नई list बनाता है और वही list लक्ष्य slice को असाइन की जाती है। अगर आप दाईं ओर अलग से list बनाए बिना काम करना चाहते हैं, तो सीधे कोई iterable दें—जैसे ऐसा जेनरेटर जो slice लेने की बजाय इंडेक्स से तत्व दे। इस तरह आप slice असाइनमेंट तो करते हैं, पर उसे पहले से बनी list की जगह एक iterator से फीड करते हैं।
seq = [...]  # कोई मौजूदा सूची
def iter_window(source, lo, hi):
    for pos in range(lo, hi):
        yield source[pos]
seq[0:10] = iter_window(seq, 10, 20)यह तरीका पहले slicing से list बनाए बिना, सीधे इंडेक्स के आधार पर तत्व चुनता है। समय के साथ CPython में निष्पादन ओवरहेड कम हुआ है, इसलिए जेनरेटर से slice असाइनमेंट को फीड करना एक व्यवहार्य पैटर्न हो सकता है, और कम से कम यह स्पष्ट semantics रखते हुए अनुकूलन‑अनुकूल है।
एक और अधिक चरम विचार ctypes के जरिए सीधे मेमरी ऑपरेशन्स से पॉइंटर्स को 'पेस्ट' करने का है। अवधारणा यह होगी कि पहले एक source slice बनाया जाए ताकि reference counts सही रहें, फिर लक्ष्य रेंज में None जैसे अमर (immortal) ऑब्जेक्ट को रखें ताकि समय से पहले decref न हो, फिर ctypes.memmove से आंतरिक पॉइंटर्स ओवरराइट कर दिए जाएँ और अंत में source slice साफ किया जाए। सिद्धांततः संभव होते हुए भी, यह Python की पहले से दी हुई गारंटियों को फिर से लागू करने जैसा भारी काम है—व्यावहारिक लाभ अत्यल्प और जोखिम कहीं ज़्यादा।
अंत में, ध्यान दें कि यह चर्चा Python lists के बारे में है। कच्चे संख्यात्मक डेटा की arrays अलग होती हैं। संख्यात्मक कामों के लिए standard library की arrays और आमतौर पर NumPy arrays कच्चा डेटा रखती हैं, ऑब्जेक्ट रेफ़रेंसेज़ नहीं। इससे तेज़, लगातार (contiguous) मेमरी ऑपरेशन्स संभव होते हैं। NumPy में arrays के बीच slice असाइनमेंट यह पहचान सकता है कि दोनों तरफ arrays हैं और कुशल कॉपी कर सकता है, और slice लेना अक्सर कॉपी के बजाय एक view देता है। यदि आपका उपयोग मामला संख्यात्मक है, तो प्रायः यही सही औज़ार है।
क्यों मायने रखता है
परिवर्तन (mutation), अस्थायी संरचनाओं और प्रदर्शन को लेकर आपकी अपेक्षाएँ यह तय करती हैं कि आप कोड कैसे रचते हैं। यदि आप list slices के लिए in-place 'memmove‑style' ऑपरेशन मानकर चलते हैं, तो गलत मानसिक मॉडल और अप्रत्याशित नतीजे मिल सकते हैं। यह जानना कि दाईं ओर का slice एक नई list बनाता है, समय और मेमरी—दोनों लागतें साफ करता है और यह तय करने में मदद देता है कि कब जेनरेटर‑आधारित iterable, एक साधारण for‑loop, या डोमेन‑उपयुक्त array प्रकार बेहतर बैठता है।
मुख्य बातें
lists के साथ slice असाइनमेंट में दाईं ओर पहले मूल्यांकन होता है और एक नई list बनती है। CPython का disassembly अलग slice read और उसके बाद slice write दिखाता है—कोई संयुक्त in-place मूव नहीं। क्योंकि list के तत्व references होते हैं जिनके काउंट समायोजित होने चाहिए, और क्योंकि slice असाइनमेंट सूची का आकार बदल सकता है, एक सार्वभौमिक in-place अनुकूलन Python lists का तरीका नहीं है। यदि दाईं ओर list बनाना टालना है, तो iterator से तत्व yield करें। और यदि आपको संख्यात्मक डेटा के लिए कच्ची, contiguous मेमरी जैसी semantics चाहिए, तो उसी काम के लिए बनी arrays—आम तौर पर NumPy—का उपयोग करें। सबसे बढ़कर, वास्तविक semantics को ध्यान में रखकर लिखें ताकि आपका कोड सही और साफ बना रहे।
यह लेख StackOverflow पर दिए गए प्रश्न (लेखक: user29120650) और jsbueno के उत्तर पर आधारित है।