写Python这么多年,最容易翻车的地方往往都是些看起来特简单的小功能。比如列表去重,我见过太多人随手一写就埋坑了,今天刚好借个实际脚本聊聊这事儿。
先看最常见的写法:
def dedupe_basic(items):"""最常见的去重——直接丢进set再转回来"""return list(set(items))
这玩意儿在面试题里出现率高达99%,但用过的人都知道第一个大坑:顺序丢了。你传进去的是 [2, 1, 3, 1, 2],出来的可能是 [1, 2, 3]。数据顺序一旦乱了,后面逻辑就全歪了,尤其在处理时间序列、日志、配置文件这些对顺序敏感的场景。
于是有经验的人会换一招,用字典保持插入顺序(Python 3.7+ dict是有序的):
def dedupe_ordered(items):"""利用dict的key唯一性,同时保持插入顺序"""return list(dict.fromkeys(items))
这个写法干净利落,在绝大多数纯值去重场景下表现很好。但它有第二个大坑:碰上不可哈希的元素直接炸。比如你的列表里有个字典或者列表本身,dict.fromkeys 直接甩给你一个 TypeError: unhashable type: 'dict'。你写个爬虫抓了一堆数据,每个元素是个字典,想去个重,用这方法程序就跪了。
不少新手会退化成双重循环的笨办法:
def dedupe_slow(items):"""最笨的去重——徒手遍历判断"""result = []for item in items:if item not in result:result.append(item)return result
功能上似乎没毛病,字典列表也能去重了,顺序也保持了,但这是第三个大坑,藏得更深:时间复杂度O(n²)。列表一大,比如几千个元素,循环里的 item not in result 每次都在做线性扫描,运行时间直接爆炸。你写个数据分析脚本,10万条记录跑几分钟出不来,用户还以为死机了。
那有没有一个写法,既能保持顺序,又能处理不可哈希的元素,还不至于慢成龟?我项目中用到的这个脚本就是个典型例子,应对的就是这种“混合列表去重”场景——经常处理API返回的复杂数据结构,里面元素可能是字符串、数字、元组、字典,甚至层层嵌套的东西。
核心思路是这样的:用一个 seen 集合记录已经见过的元素,但普通集合只能放可哈希的东西,遇到字典这种“刺头”就单独处理。脚本里我这样写的:
def robust_dedupe(items, key=None):"""通用列表去重,保持顺序,兼容不可哈希元素key: 可选的规范化函数,复杂对象可以自定义去重依据"""seen = set() # 用来记录可哈希的元素或标识seen_unhashable = [] # 兜底:记录不可哈希对象的身份result = []for item in items:# 计算当前元素的“标识”,用于判断是否重复if key:identifier = key(item)else:identifier = item# 尝试将标识加入集合,不可哈希就坠入备选逻辑try:if identifier not in seen:seen.add(identifier)result.append(item)except TypeError:# 不可哈希元素:退化成线性扫描,但因为这类元素通常不多,影响可控if identifier not in seen_unhashable:seen_unhashable.append(identifier)result.append(item)return result
这段代码的亮点在于分而治之。大部分元素还是走集合判断,保持O(1)的平均查找速度。只有碰到 TypeError(也就是不可哈希的元素)才会落进备用的列表扫描。实际场景里不可哈希元素往往只占少数,所以整体性能依然接近线性。key 参数还给了额外灵活性,比如你要根据字典里的某个字段去重,直接传个 lambda x: x['id'] 就行。
我试过一个极端例子:列表里混了字符串、数字、以及几百个重复的字典,用 robust_dedupe 跑下来0.05秒,而那个双重循环的写法用了将近8秒。差距就是这么大。
当然这个脚本也不是万能药。如果你列表里几乎全是不可哈希的东西,那备用扫描又变成了O(n²),这时候就得换个思路了——比如用 repr 序列化后做集合键,但那是另一个话题。
说到底,列表去重这事就像吃饭用筷子——怎么都能夹起来,但姿势不对要么别扭要么洒一地。下次写去重别直接 list(set(...)) 一把梭了,先想清楚你的数据长什么样、对顺序有没有要求、元素能不能哈希,再用对应的写法。我的经验是,能控制输入结构就尽量用 dict.fromkeys,实在复杂了再用分治的 robust_dedupe,保持简单,别过度设计。
这种小脚本平时不起眼,但攒多了就是效率的护城河。你平时去重都会踩什么坑?可以一起聊聊。
