给 Python 列表去重又想保留原来的先后次序,最稳的写法是 list(dict.fromkeys(seq)):它按首次出现顺序保留元素,把重复项丢弃,且不改变类型。本文先把坑讲清,再给可复现的几种写法。

你手上有 ids = [3, 1, 3, 2, 1, 4],只想留下每个数字第一次出现的位置,得到 [3, 1, 2, 4]。很多人第一反应是 set(ids),结果顺序全乱了;更麻烦的是,这个「乱」在不同环境里还不一定复现,排查时根本无从下手。今天这篇文章,编程狮就把 Python 列表去重里「保持顺序」这件事讲透,让你不再靠运气,也不再被面试或生产环境里偶发的顺序问题卡住。
一、先看清「去重」和「保序」是两件事
去重只关心「每个值留一份」,保序还多了一层要求:留下的那份,必须是它第一次出现时的位置。两者合起来,才叫保持顺序去重。
用一个例子体会差别:[3, 1, 3, 2, 1, 4] 去重保序得到 [3, 1, 2, 4],而只去重不保序可能得到 [1, 2, 3, 4] 或 [4, 3, 2, 1] 等任意顺序。只要你的下游要「按原顺序展示」或「保留用户操作先后」,就必须保序,否则前端列表、日志回放、答题顺序都会错位。
⚠️ 注意:set 在 Python 3.7+ 的 CPython 里确实按插入顺序保存,但这是一个实现细节,不是语言保证的语义。依赖它来「保序」属于踩边界,换实现或早期版本就会翻车。
如果你对字典和列表的基础还不熟,可以先过一遍 Python3 基础教程,把这两个结构过一遍再回来。
二、方案一:dict.fromkeys 保序去重,首选
dict.fromkeys 会按你给的序列顺序建立键,而字典的键天然不重复。把它套一层 list(),就得到了去重且保序的列表。
ids = [3, 1, 3, 2, 1, 4]
result = list(dict.fromkeys(ids))
print(result)
上面这段做了什么:先按 ids 的顺序建出键 {3: None, 1: None, 2: None, 4: None},再取键转成列表。执行后你应该看到 [3, 1, 2, 4]。重复项被丢掉,顺序完全保留。
这个写法不仅限于数字,字符串、元组等可哈希元素同样适用;只要元素能作为字典键,dict.fromkeys 就能帮你保序去重,比如对 ['a', 'b', 'a', 'c'] 会得到 ['a', 'b', 'c']。如果元素本身不可哈希(例如嵌套列表),它就无能为力,那要回到「推导式 + seen 集合」或先转成可哈希类型。
从复杂度看,dict.fromkeys 一次建字典,时间开销 O(n),比「先建空列表再循环判断」更短也更不容易写错;空间上只多存一份键的引用,几乎可以忽略。所以它不只是「最推荐」,在实际项目里也是默认首选。只有当你需要边去重边做字段转换、过滤或聚合时,才值得退回到显式循环——那时候可读性和可控性比「一行搞定」更重要。另外要小心一个联动坑:dict.fromkeys(seq, value) 里所有键会共享同一个 value 对象,如果传的是可变对象(如列表),会出现「改一个、全跟着变」的现象;需要独立值时,应该用推导式逐个新建,而不是共用同一个引用。
需要查具体语法的细节,Python 速查手册 可以放在手边。这个写法兼容 Python 3.7+,是当前最推荐的一行方案。

三、方案二:列表推导式显式保序
如果你想在去重的同时顺手做点别的(比如只保留偶数),列表推导式最清楚。用一个集合记录「已经见过的」,只放行第一次出现的元素。
ids = [3, 1, 3, 2, 1, 4]
seen = set()
result = []
for x in ids:
if x not in seen:
seen.add(x)
result.append(x)
print(result)
想立刻验证,可以打开 Python 在线运行 把代码粘进去跑。执行后同样是 [3, 1, 2, 4]。这种写法最直观,逻辑一眼能看懂,适合给初学者讲原理。
它的代价是多了一个 seen 集合和几行循环,但换来的是完全可控:你可以随时在上面加条件,比如「只保留大于 1 的数」。
举个例子,想要「去重 + 只保留偶数」,直接在放行条件里加一句即可:
ids = [3, 1, 3, 2, 1, 4]
seen = set()
result = []
for x in ids:
if x not in seen and x % 2 == 0:
seen.add(x)
result.append(x)
print(result) # [2, 4]
这段把「是否见过」和「是否满足业务条件」合成一个判断,去重和过滤一步到位,顺序依旧按首次出现保留。
四、方案三:直接 set 去重,但要接受丢顺序
set(ids) 是最短的去重写法,但它只保证「每个值一份」,不保证顺序。
ids = [3, 1, 3, 2, 1, 4]
result = list(set(ids))
print(result)
| 方式 | 保序 | 代码量 | 适合场景 |
|---|---|---|---|
list(dict.fromkeys(s)) |
是 | 少 | 通用首选,要保序 |
| 推导式 + seen 集合 | 是 | 多 | 去重时还要额外过滤 |
list(set(s)) |
否 | 最少 | 只关心唯一值、不关心顺序 |
如果顺序对你无所谓(比如只是统计有哪些不同取值),Python 入门教程 里也常这样写,简单直接。但只要涉及「按出现顺序展示」,就别用 set。
五、踩坑清单
三个最常翻车的地方,每个给现象、原因和办法。
- 现象:去重后顺序跟原列表不一样。原因:用了
set,它不保序。解决:换成dict.fromkeys。 - 现象:嵌套列表去重报错或没效果。原因:列表本身不可哈希,不能放进 set 当键。解决:先转成元组,或按某个字段去重。比如对
[["a"], ["a"], ["b"]]去重,可以先[tuple(x) for x in data]转成元组再进 set,或者直接用「按字段去重」的思路处理对象列表:
data = [{"id": 1, "name": "x"}, {"id": 1, "name": "x"}, {"id": 2, "name": "y"}]
seen = set()
result = [d for d in data if not (d["id"] in seen or seen.add(d["id"]))]
print([d["name"] for d in result]) # ['x', 'y']
这种「按字段去重」在接口返回重复记录时特别常用,核心还是「seen 集合记录已出现过的键」,只是键从元素本身换成业务字段。
- 现象:想「保留最后一次出现」而不是第一次。原因:
fromkeys天然留首次。解决:先ids[::-1]反转,去重后再反转回来。
验证最直接:挑一个已知序列,比如 [2, 2, 1],去重后应当得到 [2, 1] 而不是 [1, 2]。预期结果(未在本机执行)是保留每个值第一次出现的位置,且长度等于不同值的个数。
写完后 检查 一下结果顺序是否和首次出现一致;如果数据来自配置 文件,先解析再处理;万一 失败 地遇到不可哈希元素,记得改成按 key 函数去重,而不是硬塞进集合。
总结
Python 列表去重要保序,首选 list(dict.fromkeys(seq)):一行、可读、稳定。需要边去重边过滤就用推导式加 seen 集合;完全不在乎顺序才用 set。核心是分清「去重」和「保序」是不是同一件事。
要点带走:
- dict.fromkeys 按首次出现顺序保留,最稳;
- set 只去重不保序,别用来排序展示;
- 嵌套结构先转可哈希类型再处理。
下一步可以看看怎么把去重和前面的「多列表合并」串起来,先汇总再去重,一条流水线搞定。
延伸学习
想把这块知识系统补齐,可以按这个顺序来:
- 先过一遍 Python 系统学习路径,把集合、字典、列表的关系铺平;
- 想看更多保序写法,翻 Python 列表保序去重;
- 想跟着课程边学边练,Python 配套课程 是图文微课形式,适合巩固。
常见问题
Q:为什么不该直接用 set 去重?
A:set 只保证每个值留一份,不保证顺序。只要你要「按原列表出现顺序展示」,set 就会让结果乱掉,所以保序场景要换成 dict.fromkeys。
Q:dict.fromkeys 在低版本 Python 能用吗?
A:保序依赖于 Python 3.7+ 的字典插入顺序语义。更早的版本里,建议用「推导式 + seen 集合」这种显式写法,完全不依赖语言实现细节。
Q:怎么保留最后一次出现而不是第一次?
A:先把列表反转,去重,再反转回来。这样每个值留下的是它在反转序列里第一次出现的位置,也就是原序列最后一次出现的位置。

TRAE-AI编程



