Python 列表去重还怕乱序?先看清这 3 个坑再动手

编程狮(w3cschool.cn) 2026-10-15 07:08:51 浏览数 (77)
反馈

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

Python 列表保序去重方法图

你手上有 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+,是当前最推荐的一行方案。

Python 列表去重方式选择图

三、方案二:列表推导式显式保序

如果你想在去重的同时顺手做点别的(比如只保留偶数),列表推导式最清楚。用一个集合记录「已经见过的」,只放行第一次出现的元素。

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 只去重不保序,别用来排序展示;
  • 嵌套结构先转可哈希类型再处理。

下一步可以看看怎么把去重和前面的「多列表合并」串起来,先汇总再去重,一条流水线搞定。

延伸学习

想把这块知识系统补齐,可以按这个顺序来:

  1. 先过一遍 Python 系统学习路径,把集合、字典、列表的关系铺平;
  2. 想看更多保序写法,翻 Python 列表保序去重;
  3. 想跟着课程边学边练,Python 配套课程 是图文微课形式,适合巩固。

常见问题

Q:为什么不该直接用 set 去重?

A:set 只保证每个值留一份,不保证顺序。只要你要「按原列表出现顺序展示」,set 就会让结果乱掉,所以保序场景要换成 dict.fromkeys。

Q:dict.fromkeys 在低版本 Python 能用吗?

A:保序依赖于 Python 3.7+ 的字典插入顺序语义。更早的版本里,建议用「推导式 + seen 集合」这种显式写法,完全不依赖语言实现细节。

Q:怎么保留最后一次出现而不是第一次?

A:先把列表反转,去重,再反转回来。这样每个值留下的是它在反转序列里第一次出现的位置,也就是原序列最后一次出现的位置。

0 人点赞