Python 保序去重:dict.fromkeys 的适用边界与测试方法
先明确“重复”的含义
处理一组标签、编号或命令行参数时,经常希望删除重复项,同时保留每个值第一次出现的位置。这里的顺序不是排序结果,而是输入顺序。例如输入 [3, 1, 3, 2, 1],期望输出 [3, 1, 2]。本文讨论内存中的教学数据,不涉及线上业务或性能实测。
一个短小实现
def unique_in_order(items):
return list(dict.fromkeys(items))
values = [3, 1, 3, 2, 1]
result = unique_in_order(values)
assert result == [3, 1, 2]
print(result)
dict.fromkeys 把输入元素用作字典键,值默认为 None。相等的键不会产生新的位置,首次插入顺序被保留。Python 3.7 起,字典保持插入顺序是语言保证;再对字典调用 list,得到的就是键列表。
这个最小示例在本次 DevSpace 免费体验环境中已保存为 Python 文件并运行,输出 [3, 1, 2],断言通过。它验证的是这个输入,不代表所有输入类型都适用。
边界一:元素必须可哈希
整数、字符串通常可以用作键;列表和字典不能直接用作键。输入 [[1, 2], [1, 2]] 时,上面的函数会抛出 TypeError。元组也不是天然都可哈希,例如包含列表的元组仍然不可哈希。
如果输入是记录,应先明确业务键。例如按编号去重,并保留第一条记录:
def first_by_id(records):
seen = set()
result = []
for record in records:
key = record["id"]
if key not in seen:
seen.add(key)
result.append(record)
return result
此处要求每条记录存在 id,且 id 可哈希。缺失字段、空编号和大小写是否等价,都应在调用前明确,而不是悄悄吞掉异常。result 中保存的是原记录引用,不是深拷贝。
边界二:类型不同也可能被视为同一个键
字典去重依据哈希和相等关系,而非显示形式或类型名称。例如 1 == 1.0 == True 为真,它们作为字典键时会合并。如果业务要求整数 1 与布尔值 True 不同,可考虑使用 (type(value), value) 作为去重键,但 value 本身仍需可哈希。这个策略也必须符合业务定义,不能只为了消除报错而随意转换成字符串。
边界三:去重与计数是不同需求
保序去重只保留“是否出现过”,不会保留出现次数;需要频次时应选择 Counter 等计数方式。list(set(items)) 虽然能去重,却不能作为保留首次出现顺序的接口承诺。sorted(set(items)) 又引入了排序,并要求值之间可比较。三个目标不应混用。
用断言把约定写清楚
下面是可自行运行的补充测试,不把它们当作上文已执行的测试结果:
assert unique_in_order([]) == []
assert unique_in_order(["b", "a", "b"]) == ["b", "a"]
assert len(unique_in_order([1, 1.0, True])) == 1
try:
unique_in_order([[1], [1]])
except TypeError:
pass
else:
raise AssertionError("列表元素应触发不可哈希错误")
一般情况下,哈希查找使整体时间近似线性,额外空间随不同元素的数量增加;这不是对恶意哈希碰撞或昂贵自定义相等比较的最坏情况保证。超大输入还要考虑是否真的需要一次性创建完整结果列表。
结论很简单:先定义相等规则、保留首次还是末次、是否需要次数,再选择容器。短代码值得用,但它的输入契约应当比代码更清楚。
- 点赞
- 收藏
- 关注作者
评论(0)