You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

生成Account对象树时触发RecursionError的原因排查

问题描述

我正在开发一个程序,用于读取会计科目表并将其转换为Account对象树,后续将基于该树进行操作。会计科目表包含多层子科目,每个子科目的名称比其父科目多缩进3个空格,示例如下:

Current Assets
      Cash
         1000000.00 - Cash
         1000010.00 - Bank Account #1
         ...
      Other Current Assets
         Receivables
            100800.00 - Accounts Receivable
            ...

编写的Account类如下:

class Account:
    parent = None
    num: int | None
    name: str  # Name after stripping whitespace and splitting off account number
    raw: str   # Name prior to ^
    ...  # Assorted irrelevant class variables
    acct_type: str | None
    detail_type: str | None
    _header: bool = False  # Flags whether an account's a header account
    descendants: list = list() # Holds account's children. Should be empty if _header == False.

    def __init__(self, instr: str, parent=None, acct_type: str = None, detail_type: str = None):
        ''' Initialize an Account.
        
        - parent: Account. `None` if top-level.
        - instr: raw string from ACS CoA.
        - acct_type: QBO Account Type
        - detail_type: QBO Detail Type
        '''
        self.raw = instr
        self.parent = parent
        self.acct_type = acct_type
        self.detail_type = detail_type
        stripped = instr.strip()
        ...  # Assorted initialization code, incl. manipulating stripped 
             # to get values for self.num and self.name
    ...  # Assorted irrelevant methods
    def is_header(self) -> bool:
        return self._header
    def depth(self) -> int:
        return (len(self.raw) - len(self.raw.strip())) / 3 # self.raw
    def add_descendant(self, instr, acct_t: str = None, detail: str = None): 
        '''Add descendant to account.

        If `acct_type` and `detail_type` are supplied, will override parent account type.
        '''
        if len(self.descendants) > 0:
            if calc_depth(instr) > self.descendants[-1].depth():
                self.descendants[-1].add_descendant(instr, acct_t = acct_t, detail = detail)
            else:
                self.descendants.append(Account(
                    instr,
                    parent=self, 
                    acct_type = self.acct_type if acct_t is None else acct_t,
                    detail_type = self.detail_type if detail is None else detail
                ))
        else:
            self.descendants.append(Account(
                instr,
                parent=self, 
                acct_type = self.acct_type if acct_t is None else acct_t,
                detail_type = self.detail_type if detail is None else detail
            ))
        return

if __name__ == "__main__":
    accounts: list[Account] = list()
    for (acs_name, account_type, detail_type) in get_data(): # Gets data as list[tuple[str, str, str]]
        d = len(acs_name) - len(acs_name.lstrip())) / 3  # Depth of current row's account
        if d == 0:  # Overall header
            accounts.append(Account(acs_name, parent=None, acct_type=account_type, detail_type=detail_type))
        else:
            if account_type is not None:
                # Account metadata supplied in CoA spreadsheet
                accounts[-1].add_descendant(
                    acs_name,
                    acct_t = account_type,
                    detail = detail_type)
            else:
                # Impute account metadata from parent
                accounts[-1].add_descendant(acs_name)

运行时触发RecursionError,错误栈显示add_descendant方法无限递归。调试发现二级科目会不断将自身添加为后代,生成的树结构如下:

<__main__.Account object at 0x0000015631511BE0>   # Current Assets
  <__main__.Account object at 0x0000015631796710>   # Cash
    <__main__.Account object at 0x0000015631796710>   # Cash
      <__main__.Account object at 0x0000015631796710>   # Yet more Cash
        ...  # 980-ish more Cash

预期的树结构应为:

<__main__.Account object at 0x0000015631511BE0>   # Current Assets
  <__main__.Account object at 0x0000015631796710>   # Cash
    <__main__.Account object at 0x0000015631XXXXXX>   # 1000000.00 - Cash
问题分析与解决方案

核心错误:类变量与实例变量混淆

你在Account类中定义的parent、descendants等是类变量,所有Account实例会共享同一个descendants列表,而不是每个实例拥有独立的列表。这就导致当你向某个实例的descendants添加元素时,所有实例的descendants都会被修改,最终引发无限递归。

修复步骤:

  1. 将类变量改为实例变量:在__init__方法中初始化parent、descendants等变量,确保每个实例拥有独立的属性:
class Account:
    # 移除类级别的parent、descendants定义
    num: int | None
    name: str
    raw: str
    acct_type: str | None
    detail_type: str | None
    _header: bool = False

    def __init__(self, instr: str, parent=None, acct_type: str = None, detail_type: str = None):
        self.raw = instr
        self.parent = parent  # 实例变量
        self.acct_type = acct_type
        self.detail_type = detail_type
        self.descendants = []  # 每个实例独立的列表
        stripped = instr.strip()
        ...  # 原有初始化代码
  1. 修复calc_depth未定义问题:代码中调用了calc_depth(instr)但未实现该函数,需要添加一个计算输入字符串深度的辅助函数,或者直接在add_descendant中计算:
def get_str_depth(instr: str) -> int:
    return (len(instr) - len(instr.strip())) // 3  # 用整数除法避免浮点

# 在add_descendant中替换calc_depth调用:
def add_descendant(self, instr, acct_t: str = None, detail: str = None): 
    current_depth = get_str_depth(instr)
    if len(self.descendants) > 0:
        last_child_depth = self.descendants[-1].depth()
        if current_depth > last_child_depth:
            self.descendants[-1].add_descendant(instr, acct_t, detail)
        else:
            self.descendants.append(Account(
                instr,
                parent=self, 
                acct_type=self.acct_type if acct_t is None else acct_t,
                detail_type=self.detail_type if detail is None else detail
            ))
    else:
        self.descendants.append(Account(
            instr,
            parent=self, 
            acct_type=self.acct_type if acct_t is None else acct_t,
            detail_type=self.detail_type if detail is None else detail
        ))
  1. 修正主逻辑中的深度计算:主循环中的深度计算多了一个右括号,需要修正:
d = (len(acs_name) - len(acs_name.lstrip())) // 3  # 修正括号,用整数除法

额外优化建议

  • 维护当前节点路径:按顺序处理科目时,建议维护一个当前节点的栈(路径),根据当前深度调整栈的长度,这样比递归调用add_descendant更直观,也避免递归深度问题:
if __name__ == "__main__":
    accounts: list[Account] = list()
    current_path = []  # 保存当前层级的节点栈
    for (acs_name, account_type, detail_type) in get_data():
        d = (len(acs_name) - len(acs_name.lstrip())) // 3
        # 调整当前路径到对应深度
        while len(current_path) > d:
            current_path.pop()
        if d == 0:
            new_acct = Account(acs_name, parent=None, acct_type=account_type, detail_type=detail_type)
            accounts.append(new_acct)
            current_path = [new_acct]
        else:
            parent_acct = current_path[-1]
            new_acct = Account(
                acs_name,
                parent=parent_acct,
                acct_type=account_type if account_type is not None else parent_acct.acct_type,
                detail_type=detail_type if detail_type is not None else parent_acct.detail_type
            )
            parent_acct.descendants.append(new_acct)
            current_path.append(new_acct)

内容的提问来源于stack exchange,提问作者In Hoc Signo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 13:45:54