在处理多层级的配置树、组织架构或销售报表时,经常需要把分散在各层的记录汇总成一张平铺的数据行列表。传统写法习惯在类里放一个列表,递归时不断往里面append,但这会让函数产生副作用。纯递归累积的核心思路是:每一层递归都返回自己收集到的行,由调用方负责拼接,全程不碰任何对象状态。

为什么不应依赖类属性做累积
把累积容器写成类的字段,例如self.rows,初看省事,实则埋下多处隐患。首先,同一个解析器实例若在多线程环境下被复用,多个线程会同时修改self.rows,导致数据错乱或丢失。其次,函数不再是纯函数,调用前后对象状态变化,单元测试必须重置实例,重构成本高。
更严重的是,一旦递归中途因为异常退出,类属性里可能残留了部分数据,下次调用就会混进旧结果。下面这段示例代码展示了典型的“有状态”反模式:
class BadCollector:
def __init__(self):
self.rows = []
def walk(self, node):
if 'value' in node:
self.rows.append(node['value'])
for child in node.get('children', []):
self.walk(child)
data = {'value': 1, 'children': [{'value': 2}, {'value': 3}]}
c = BadCollector()
c.walk(data)
print(c.rows)
纯递归返回累积结果的基本写法
去掉类属性后,我们可以让递归函数始终返回一个列表。当前节点若自身有数据行,就把它放进新列表;再遍历子节点,把子节点返回的列表通过加号或extend合并进来。这样每一层调用都只处理自己视野内的数据,并通过返回值向上传递。
以下代码演示了最直观的纯递归累积方式,不依赖任何外部可变状态:
def collect_rows(node):
rows = []
if 'value' in node:
rows.append(node['value'])
for child in node.get('children', []):
rows = rows + collect_rows(child)
return rows
data = {
'value': 1,
'children': [
{'value': 2},
{'value': 3, 'children': [{'value': 4}]}
]
}
result = collect_rows(data)
print(result)
使用加号会创建新列表,逻辑清晰但频繁分配内存。若数据层级很深、节点很多,可改用局部列表加extend,在保持纯递归返回的同时减少对象创建:
def collect_rows_extend(node):
rows = []
if 'value' in node:
rows.append(node['value'])
for child in node.get('children', []):
rows.extend(collect_rows_extend(child))
return rows
用参数携带累加容器减少拷贝
如果不希望每层都生成新列表,还可以把累加容器作为参数传入。注意这里容器是在本次调用的参数里局部创建并向下传递的,并非类属性,因此依然符合“不依赖类属性”的要求,且线程安全。
示例里我们在入口函数外包一层,内部递归函数接收acc参数,直接往里追加,最终返回同一引用:
def collect_rows_param(node):
def _walk(n, acc):
if 'value' in n:
acc.append(n['value'])
for child in n.get('children', []):
_walk(child, acc)
return acc
return _walk(node, [])
sample = {'value': 'A', 'children': [{'value': 'B'}, {'value': 'C'}]}
print(collect_rows_param(sample))
这种方式避免了中间列表拼接,性能更好,同时因为没有使用类成员变量,多个线程各自触发collect_rows_param时互不干扰。唯一需要注意的是,返回的是同一列表引用,调用方不应意外持有并修改它。
处理更复杂嵌套行的实践建议
真实场景里“数据行”往往不是单值,而是字典或对象。此时只需把append的内容换成整行记录,并在递归前补充层级字段。例如给每行打上深度标记:
def collect_detail_rows(node, depth=0):
rows = []
if 'record' in node:
row = dict(node['record'])
row['depth'] = depth
rows.append(row)
for child in node.get('children', []):
rows.extend(collect_detail_rows(child, depth + 1))
return rows
tree = {
'record': {'id': 1},
'children': [
{'record': {'id': 2}},
{'record': {'id': 3}, 'children': [{'record': {'id': 4}}]}
]
}
for r in collect_detail_rows(tree):
print(r)
这种写法把递归深度、父路径等上下文作为普通参数传递,完全不需要在类里维护状态。如果后续要支持过滤、字段映射,只要在rows.append前加判断或转换即可,函数依旧保持纯净和可测试性。
方案对比与选型
我们将三类做法放在一张表里对照,方便根据场景取舍:
| 方式 | 是否依赖类属性 | 线程安全 | 内存开销 |
|---|---|---|---|
| 类属性追加 | 是 | 否 | 低 |
| 返回值加号拼接 | 否 | 是 | 中 |
| 参数传容器 | 否 | 是 | 低 |
对于脚本工具、后端接口中的一次性树形展开,推荐参数传容器写法,兼顾效率与安全。若逻辑非常简单且不在并发环境运行,返回值拼接更易读。无论如何,避免把累积结果挂到类实例上,是写出可维护递归代码的第一步。
recursionnested_dataaccumulation修改时间:2026-08-04 11:03:30