2025, Oct 18 16:31
chart of accounts से नेस्टेड ट्री बनाते समय Python में RecursionError: कारण और साफ़ समाधान
Python में chart of accounts से ट्री बनाते समय shared children सूची से RecursionError क्यों होता है, और क्लास‑लेवल mutable को __init__ में अलग कर इसे फिक्स करें.
खातों की सूची (chart of accounts) से एक नेस्टेड ट्री बनाना सुनने में आसान लगता है, जब तक कि संरचना खुद की नकल करने न लगे और स्टैक ओवरफ्लो न हो जाए। Python में एक क्लासिक गलती इतनी बारीक होती है कि कोड‑रिव्यू से निकल जाती है, लेकिन प्रोडक्शन में RecursionError करवा देती है। नीचे एक छोटा‑सा वॉकथ्रू है: इंडेंटेशन की गहराई से ट्री बनाते समय ऐसा क्यों होता है और इसे साफ तरीके से कैसे ठीक करें।
समस्या की रूपरेखा
सोर्स डेटा एक chart of accounts है, जिसमें hierarchy इंडेंटेशन से तय होती है। हर स्तर तीन स्पेस से इंडेंट किया गया है। उद्देश्य है: हर पंक्ति को पार्स करना, उसकी गहराई पहचानना, और उसे सही पैरेंट नोड के भीतर नेस्ट करना।
यहाँ उसी तरीके को दर्शाता एक संक्षिप्त क्लास और ड्राइवर कोड है:
class ChartItem:
ancestor = None
acct_no: int | None
label: str
raw_line: str
kind: str | None
subtype: str | None
is_header_flag: bool = False
children: list = list() # प्रत्येक इंस्टेंस के children को रखने के लिए अभिप्रेत
def __init__(self, line_in: str, ancestor=None, kind: str = None, subtype: str = None):
self.raw_line = line_in
self.ancestor = ancestor
self.kind = kind
self.subtype = subtype
cleaned = line_in.strip()
... # cleaned से self.acct_no और self.label निकालने वाला कोड
def is_header(self) -> bool:
return self.is_header_flag
def nesting_level(self) -> int:
return (len(self.raw_line) - len(self.raw_line.strip())) / 3
def add_child(self, line_in, type_hint: str = None, detail_hint: str = None):
if len(self.children) > 0:
if calc_level(line_in) > self.children[-1].nesting_level():
self.children[-1].add_child(line_in, type_hint=type_hint, detail_hint=detail_hint)
else:
self.children.append(ChartItem(
line_in,
ancestor=self,
kind=self.kind if type_hint is None else type_hint,
subtype=self.subtype if detail_hint is None else detail_hint
))
else:
self.children.append(ChartItem(
line_in,
ancestor=self,
kind=self.kind if type_hint is None else type_hint,
subtype=self.subtype if detail_hint is None else detail_hint
))
return
if __name__ == "__main__":
root_nodes: list[ChartItem] = list()
for (row_text, type_hint, subtype_hint) in supply_rows():
lvl = (len(row_text) - len(row_text.lstrip())) / 3
if lvl == 0:
root_nodes.append(ChartItem(row_text, ancestor=None, kind=type_hint, subtype=subtype_hint))
else:
if type_hint is not None:
root_nodes[-1].add_child(row_text, type_hint=type_hint, detail_hint=subtype_hint)
else:
root_nodes[-1].add_child(row_text)
यह क्यों क्रैश करता है
रिकर्शन उस शाखा से शुरू होता है जो तब नियंत्रण अंतिम child को सौंप देती है जब आती हुई पंक्ति उस child के nesting level से गहरी होती है। ऊपर‑ऊपर से देखें तो यह ट्री बनाने के इरादे के मुताबिक ही लगता है। मसला यह है कि प्रत्येक नोड के बच्चों को रखने वाली सूची क्लास पर घोषित है, इंस्टेंस पर नहीं। Python में क्लास पर परिभाषित mutable गुण सभी इंस्टेंस के बीच साझा होते हैं। यानी हर ChartItem एक ही children सूची की ओर इशारा करता है।
जब सूची साझा हो, तो अंतिम child में उतरने की शर्त बार‑बार उसी नोड को नियंत्रण सौंपती रहती है, जो एल्गोरिदम की नज़र में कभी sibling बन ही नहीं पाता — वह अलग‑अलग इंस्टेंस में भी वही तर्कसंगत स्लॉट बना रहता है। नतीजा: मेथड खुद को पुकारती रहती है, कॉल स्टैक बढ़ता जाता है और अंततः इंटरप्रेटर RecursionError फेंक देता है। जो लक्षण दिखते हैं — जैसे कोई नोड बार‑बार खुद के अंदर खुद को जोड़ रहा हो — वे दरअसल एक साझा children कंटेनर का सीधा नतीजा हैं।
समाधान
children कंटेनर को इंस्टेंस‑स्तरीय गुण बनाइए। इसे कंस्ट्रक्टर में इनिशियलाइज़ करें, ताकि हर नोड की अपनी अलग सूची हो। बाकी की लॉजिक वैसी ही रह सकती है।
class ChartItem:
ancestor = None
acct_no: int | None
label: str
raw_line: str
kind: str | None
subtype: str | None
is_header_flag: bool = False
def __init__(self, line_in: str, ancestor=None, kind: str = None, subtype: str = None):
self.raw_line = line_in
self.ancestor = ancestor
self.kind = kind
self.subtype = subtype
self.children: list[ChartItem] = [] # इंस्टेंस‑विशिष्ट children कंटेनर
cleaned = line_in.strip()
... # cleaned से self.acct_no और self.label निकालने वाला कोड
def is_header(self) -> bool:
return self.is_header_flag
def nesting_level(self) -> int:
return (len(self.raw_line) - len(self.raw_line.strip())) / 3
def add_child(self, line_in, type_hint: str = None, detail_hint: str = None):
if len(self.children) > 0:
if calc_level(line_in) > self.children[-1].nesting_level():
self.children[-1].add_child(line_in, type_hint=type_hint, detail_hint=detail_hint)
else:
self.children.append(ChartItem(
line_in,
ancestor=self,
kind=self.kind if type_hint is None else type_hint,
subtype=self.subtype if detail_hint is None else detail_hint
))
else:
self.children.append(ChartItem(
line_in,
ancestor=self,
kind=self.kind if type_hint is None else type_hint,
subtype=self.subtype if detail_hint is None else detail_hint
))
return
अब जब children इंस्टेंस के बीच साझा नहीं है, तो last-child डेलीगेशन इरादे के मुताबिक चलता है: अधिक गहरी पंक्तियाँ मौजूदा शाखा में नीचे जाती हैं, और समान स्तर की पंक्तियाँ मौजूदा नोड के तहत जुड़ती हैं।
यह क्यों महत्वपूर्ण है
क्लास‑स्तरीय mutability इंस्टेंसों के बीच अनदेखा coupling पैदा कर सकती है। ट्री संरचनाओं में यह चुपचाप टोपोलॉजी बिगाड़ सकती है या इंसर्ट के दौरान अनंत रिकर्शन चला सकती है। अनुभवी Python डेवलपर्स भी कभी‑कभी भूल जाते हैं कि क्लास पर घोषित सूचियाँ और डिक्शनरी, जब तक कंस्ट्रक्टर में स्पष्ट रूप से बदली न जाएँ, सभी इंस्टेंस के लिए एक ही सिंगलटन रहती हैं।
निष्कर्ष
जब रिकर्सिव इंसर्ट अनपेक्षित रूप से अनंत लूप में फँस जाए, तो शुरू में ही दो बातें जाँचें। पहला, यह सुनिश्चित करें कि कॉल्स के बीच शाखा की शर्त सच में बदल रही है। दूसरा, जो भी कंटेनर इंस्टेंस‑वार होने चाहिए, वे कंस्ट्रक्टर में इनिशियलाइज़ हों, क्लास पर नहीं। Python में क्लास‑स्कोप पर घोषित सूचियाँ और डिक्शनरी सभी इंस्टेंस में साझा होती हैं और ट्री‑बिल्डिंग जैसी stateful लॉजिक को बिगाड़ देती हैं। उन्हें __init__ में ले जाएँ, इंसर्शन के नियम सरल रखें, और hierarchy डेटा के इरादे के मुताबिक आकार लेगी।
यह लेख StackOverflow पर प्रश्न (लेखक: In Hoc Signo) और John Gordon के उत्तर पर आधारित है।