在数据处理脚本里,我们经常会拿到类似[["张三", 10], ["李四", 5], ["张三", 3]]这样的结构:外层是列表,里层每个子列表由一个姓名和一个数值组成。当同一个姓名出现多次时,需求通常不是保留多条记录,而是把数值加起来,得到每个姓名对应的总和。这个问题看似简单,但写法不同,运行效率可能差出几十倍。

一、最容易想到的双层循环写法
刚接触编程的人往往会用两层循环:外层遍历原始列表,内层去结果列表里找有没有同名的项,有就加,没有就添。这种思路直观,但每次插入或更新都要扫描一遍结果列表,整体时间复杂度是O(n²)。当数据只有几十行时感觉不出来,一旦到了几千、上万行,脚本就会明显卡顿。
下面是一段典型的低效实现。我们用一个result列表保存合并后的数据,每次都靠for循环去比对姓名:
data = [["张三", 10], ["李四", 5], ["张三", 3], ["王五", 8], ["李四", 2]]
result = []
for item in data:
name = item[0]
value = item[1]
found = False
for r in result:
if r[0] == name:
r[1] += value
found = True
break
if not found:
result.append([name, value])
print(result)
# 输出: [['张三', 13], ['李四', 7], ['王五', 8]]
这段代码逻辑上没有问题,也很容易读懂。但它的内层循环在result变长之后会做大量无用比对。假设有1000个不同姓名、每个重复5次,内层就要执行约5000次线性搜索。如果数据再翻十倍,等待时间就会让人难以接受。
此外,由于result里存的是列表,后续若想按姓名快速取值,仍然得再写循环,不利于把聚合结果接给别的模块使用。
二、用字典将查找降为O(1)
更高效的做法是利用哈希表(在Python里就是dict)。字典的键必须是不可变类型,字符串姓名正好合适。我们让姓名作键,累加值作值,每次遇到新姓名就初始化,遇到旧姓名就直接加。哈希表的查询和插入平均是常量时间,整体复杂度降到O(n),而且代码更短。
下面是用字典改写的版本,最后如果想保持“嵌套列表”的输出形态,再转回列表即可:
data = [["张三", 10], ["李四", 5], ["张三", 3], ["王五", 8], ["李四", 2]]
agg = {}
for name, value in data:
if name in agg:
agg[name] += value
else:
agg[name] = value
# 若必须返回嵌套列表
result = [[k, v] for k, v in agg.items()]
print(result)
# 输出: [['张三', 13], ['李四', 7], ['王五', 8]]
这段代码的执行过程和前面完全不同:无论数据多大,每次循环只做一次哈希计算,不会随着已处理人数变多而变慢。在万级数据下,它通常能在几毫秒内跑完,而双层循环可能要几秒。
字典方案还有一个附带好处——聚合完的数据天然按姓名建立索引,后面若要根据姓名查总额,直接agg.get("张三")就行,不用再遍历。如果业务里姓名本身可能重复但大小写不同(如"张三"和"张三 "),可以在写入前用strip()和lower()统一清洗,避免漏合。
三、使用defaultdict进一步简化
如果觉得if name in agg的判断有点啰嗦,标准库collections里的defaultdict可以帮我们省掉这个分支。它允许在访问不存在的键时自动给一个默认初值,比如int的默认是0,这样直接加就可以。
示例代码如下,逻辑和手动判断完全一致,但可读性更高:
from collections import defaultdict
data = [["张三", 10], ["李四", 5], ["张三", 3], ["王五", 8], ["李四", 2]]
agg = defaultdict(int)
for name, value in data:
agg[name] += value
result = [[k, v] for k, v in agg.items()]
print(result)
defaultdict在内部帮你调用了int()来生成初始值0,因此第一次见到"张三"时,agg["张三"]已经是0,再加10就变成10。它并没有比普通字典更快,只是少写了判断,让聚合逻辑更聚焦于“加”这件事本身。
需要注意,defaultdict返回的键顺序在Python 3.7之前不保证与插入一致,不过现在主流版本都保留插入顺序,输出列表的顺序一般和首次出现顺序相同,满足大多数报表要求。
四、处理数值为字符串或带单位的脏数据
真实场景里,嵌套列表中的数值有时不是纯数字,可能是"10元"、"3件"这样的字符串。直接累加会报类型错误。我们可以在循环里先做清洗,把数字部分抽出来转成float或int,再走前面的字典逻辑。
下面用简单正则提取数字,并做容错处理:
import re
from collections import defaultdict
data = [["张三", "10元"], ["李四", "5"], ["张三", "3.5元"], ["王五", "8件"]]
agg = defaultdict(float)
for name, raw in data:
num_str = re.sub(r"[^0-9.]", "", raw)
if num_str == "":
continue
agg[name] += float(num_str)
result = [[k, v] for k, v in agg.items()]
print(result)
# 输出: [['张三', 13.5], ['李四', 5.0], ['王五', 8.0]]
这种写法在聚合前先归一化,避免脏数据打断主流程。如果某些记录完全无法解析,也可以选择记日志或单独收集,而不是让整个脚本崩溃。
从工程角度看,把“清洗”和“聚合”分开写函数会更清晰:一个函数负责把原始行变成(name, number),另一个只管累加。这样单测时分别验证,比揉在一起好维护。
五、性能与可读性权衡小结
面对“合并包含重复姓名的嵌套列表并累加数值”的需求,核心结论是:别用双层循环做线性搜索,改用哈希表。普通dict手动判断、defaultdict自动初值、先清洗再聚合,都是O(n)思路下的变体。它们在时间上远优于O(n²)写法,在空间上也只是多存了一份键值映射,内存开销可以忽略。
如果你的输入本来就是别的数据源(比如数据库游标、CSV读取器),甚至可以不转成嵌套列表,直接边读边合,进一步省掉中间结构。总之,认准“姓名作键、数值累加”这个模式,就能在绝大多数脚本里高效解决这类重复合并问题。
Pythonnested_listdata_aggregation修改时间:2026-08-03 02:12:34