python 列表去重怎么写?附 5 种方法+保留顺序场景对比

编程狮(w3cschool.cn) 2026-08-31 17:09:57 浏览数 (52)
反馈

列表去重,指的是把一个列表里重复出现的元素只留下一个。写法的选择取决于一个分水岭问题:你要不要保留原来的顺序。不在乎顺序,用 set() 一行就能搞定;要保留顺序,用 dict.fromkeys() 最省事,也可以自己拿一个辅助集合配合循环逐个筛选。另外要记住一条硬规则:列表里如果装的是嵌套列表这种不可哈希的元素,set() 会直接报错,得换成逐一比较的写法。

新手最容易在这里翻车的地方就是顺序。很多人第一次写去重,跑完一看"元素一个不少,结果看着挺对",就把代码提交了,直到某天要把结果和原始数据按位置对回去,才发现顺序早就乱了。今天这篇文章,编程狮就按"是否保留顺序"这条主线,把 Python 列表去重的 5 种写法讲透,最后给你一张对比表和一份踩坑清单。

Python 列表去重方法对比图

一、列表去重到底在解决什么问题

去重这件事本身不复杂,难的是去重之后你拿到的结果要长什么样。举个真实例子:从日志里按先后顺序提取出访问过的用户编号,你想知道"今天有哪些用户来过",同时也想知道"第一个访问的是谁"。这两个需求对应的写法完全不同——前者不关心顺序,后者必须保留顺序。

1.1 一个最小可运行示例

下面这段代码可以直接复制到任意 Python 3 环境里运行,两种写法的差别一眼就能看出来:

nums = [3, 1, 2, 3, 2, 1]


print(list(set(nums)))              # [1, 2, 3]
print(list(dict.fromkeys(nums)))    # [3, 1, 2]

同一个列表,第一种写法得到 [1, 2, 3],第二种得到 [3, 1, 2]。元素集合完全一样,顺序却不一样:set() 打乱了原有顺序,dict.fromkeys() 把第一次出现的顺序原样留了下来。这两行代码就是本文全部内容的缩影。

需要说明的是,第二段代码里的 dict.fromkeys(nums) 返回的是一个字典,键是 nums 里的元素、值全部是 None;再用 list() 包一层,取到的就是字典的键,也就是去重后的元素。从 Python 3.7 开始,字典明确保证保持键的插入顺序,所以这个写法才能用来保序。

1.2 常见误解

第一个误解是"set() 是按大小排序的"。它看起来有时确实像排过序,那只是巧合。集合内部用哈希表存储,遍历顺序由元素的哈希值决定。整数的哈希值就是它自己,所以小整数的顺序还勉强能猜;换成字符串就不一定了,因为 Python 的字符串哈希带随机种子,同一个程序每次运行的结果都可能不一样。所以绝对不要依赖 set() 的输出顺序。

第二个误解是"去重会修改原来的列表"。上面所有写法都是生成一个新列表,原列表纹丝不动。如果你确实想原地修改,要写成 nums[:] = list(dict.fromkeys(nums)) 这种切片赋值的形式,直接写 nums = ... 只是把变量指向了新对象,别的引用还拿着旧列表。

第三个误解是"去重能顺便把嵌套结构也处理掉"。如果列表元素是列表、字典、集合这类可变对象,它们不可哈希,set()dict.fromkeys() 都会抛出 TypeError。这是初学者实操时报错最集中的地方,应对办法放在第四节。

二、方法一:set() 转换,最省事但不保留顺序

如果你的需求只是"知道有哪些不同的值",比如统计去重后的数量、判断是否存在重复,那 set() 就是最短的写法:

user_ids = [102, 101, 103, 101, 102]


unique = list(set(user_ids))
print(unique)          # 顺序不保证,本次实测输出 [101, 102, 103]
print(len(unique))     # 3

这里实测输出是 [101, 102, 103],而原列表的第一个元素是 102——顺序已经被改变了。判断有无重复也可以不转回列表,直接比较长度:

user_ids = [102, 101, 103, 101, 102]


if len(set(user_ids)) < len(user_ids):
    print("存在重复元素")

这个写法的优点是短、快、无需导入任何模块,平均时间复杂度是 O(n)。缺点只有一个但很致命:不保留原顺序。另外它要求元素可哈希,整数、字符串、元组没问题,列表和字典不行。

三、方法二:dict.fromkeys(),一行保留顺序

要保留原顺序,最简洁的一行写法就是 dict.fromkeys()。它不需要导入任何模块,也不像后面的循环写法那样要写好几行:

user_ids = [102, 101, 103, 101, 102]


unique = list(dict.fromkeys(user_ids))
print(unique)      # [102, 101, 103]
print(len(unique)) # 3

输出是 [102, 101, 103],和原列表中每个值第一次出现的顺序完全一致。原理很简单:字典的键天然不允许重复,fromkeys() 依次把元素当作键插进去,后出现的重复键不会覆盖已有键的位置;又因为 Python 3.7 之后的字典保持插入顺序,取出来的键自然就是保序的。

这也是目前编程狮最推荐给初学者的默认写法:保序、一行、无依赖、可读性好。它同样要求元素可哈希,时间复杂度同样是 O(n)。唯一要注意的是,它只能按"值相等"去重,如果你要按对象的某个字段去重(比如一批字典里按 id 去重),就得换成第四节那种自己控制判断逻辑的写法。

实际项目里,列表去重很少单独出现,往往前面连着数据读取、后面连着排序和统计。如果你发现自己总是在这几个操作之间来回查文档,说明该把序列类操作整体梳理一遍了,Python3 教程 按场景组织的示例正好对得上这种需求。

四、方法三到方法五:可控写法、直观写法与排序去重

4.1 方法三:列表推导配合辅助集合

如果你既想保留顺序,又想在去重的同一行里加过滤条件,可以用一个辅助集合记录"已经见过的元素",写在列表推导里:

user_ids = [102, 101, 103, 101, 102]


seen = set()
result = [x for x in user_ids if x not in seen and not seen.add(x)]
print(result)      # [102, 101, 103]

这行代码能跑通,但用了个小技巧:seen.add(x) 的返回值是 Nonenot None 得到 True,所以条件成立的同时也顺手把元素加进了集合。老实说,这种写法可读性一般,团队协同时容易被人看懵,编程狮建议只在自己写的短脚本里用。

4.2 方法四:for 循环加 append,最好懂

同样是"辅助集合"的思路,展开成 for 循环就是最直白、最好维护的版本:

def dedup_keep_order(items):
    seen = set()
    result = []
    for item in items:
        if item not in seen:
            seen.add(item)
            result.append(item)
    return result


print(dedup_keep_order([102, 101, 103, 101, 102]))  # [102, 101, 103]

这个写法保留原顺序,时间复杂度 O(n),而且看得懂、好加东西:你想在去重时顺便过滤掉空值、顺便打印日志、顺便统计被去掉了几个,都只需要在循环体里加一行。

它还有一个不可替代的用途——处理不可哈希的元素。当列表里装的是嵌套列表时,set()dict.fromkeys() 都会抛 TypeError: unhashable type: 'list'。这时把辅助集合换成普通列表,用 in 做逐一比较就行:

nested = [[1, 2], [3, 4], [1, 2]]


def dedup_unhashable(items):
    result = []
    for item in items:
        if item not in result:
            result.append(item)
    return result


print(dedup_unhashable(nested))   # [[1, 2], [3, 4]]

代价是慢:每次 in 都要线性扫一遍结果列表,整体变成 O(n²)。数据量小的时候无所谓,数据量大时,可以先把内层列表转成元组(元组可哈希),再用方法二处理:

nested = [[1, 2], [3, 4], [1, 2]]
unique = [list(t) for t in dict.fromkeys(tuple(x) for x in nested)]
print(unique)      # [[1, 2], [3, 4]]

4.3 方法五:去重并排序

如果去重之后你还要结果按大小或字典序排列,那不需要分两步,直接把集合丢给 sorted()

scores = [88, 95, 88, 70, 95]


print(sorted(set(scores)))               # [70, 88, 95]
print(sorted(set(scores), reverse=True)) # [95, 88, 70]

字符串同样适用,实测 sorted(set(["Tom", "Alice", "Tom", "Bob"])) 得到 ['Alice', 'Bob', 'Tom']。注意这一步不再保留原顺序,而是换成了排序后的顺序。时间复杂度主要在排序上,是 O(n log n),比前几种略高,但你本来就要排序,这一步并没有浪费。

五、怎么选:5 种方法对比表与踩坑清单

把上面 5 种写法(外加不可哈希的应对写法)放在一起比较:

方法 保留顺序 元素能否不可哈希 时间复杂度 适用场景
set() 转换 不能 O(n) 只关心有哪些值、统计个数
dict.fromkeys() 不能 O(n) 要保留原顺序,默认首选
列表推导 + 辅助集合 不能 O(n) 保序且想在推导里加过滤条件
for 循环 + append O(n) 逻辑要展开、要加统计或日志
先去重再排序 否(改为排序) 不能 O(n log n) 去重后还要按大小排列
逐一比较(不可哈希) O(n²) 元素是嵌套列表、字典等

一句话决策:先问自己"要不要保留顺序"——不要,用 set();要,用 dict.fromkeys();元素是嵌套结构,用 for 循环逐一比较。

踩坑清单收个尾:

  1. 别依赖 set() 的输出顺序,字符串尤其不稳定,同程序多次运行结果可能不同;
  2. 旧版本 Python 不支持字典保序,dict.fromkeys() 保序依赖 Python 3.7+,更老的版本请改用循环写法;
  3. 去重不改原列表,所有方法都是返回新列表,要原地改得用切片赋值;
  4. 嵌套列表会触发不可哈希报错,先转元组或改用逐一比较;
  5. 浮点数和小数要小心0.1 + 0.20.3 不相等,看起来一样的值未必能去掉。

顺带说一句,列表去重往往只是数据清洗的第一步,后面常跟着类型转换、排序、分组。想把这套基础操作连成体系,建议顺着 Python3 教程 的列表与字典章节系统过一遍。

Python 列表去重方法怎么选

总结

列表去重只需要记住三句话:不关心顺序用 set(),一行最快;关心顺序用 dict.fromkeys(),Python 3.7+ 字典保序;元素是嵌套列表这类不可哈希对象,就退回 for 循环逐一比较。 绝大多数场景里,前两种写法已经覆盖了九成需求。

  • 是否保留顺序是选型的第一分水岭;
  • 所有去重方法都不修改原列表,而是返回新列表;
  • 不可哈希元素会让 set() 报错,先转元组或改用列表逐一比较;
  • 想同时排序就用先去重再排序的写法,代价是 O(n log n)。

延伸学习

  1. Python 基础配套课程 —— 从变量、列表一路讲到函数与文件操作。
  2. Python 列表去重方法笔记 —— 更多去重变体写法与网友补充。
  3. 文本去重工具 —— 不想写代码时,直接在线处理重复文本。

常见问题

Q:为什么我用 set() 去重之后顺序乱了,是电脑的问题吗?

A:不是,这是集合的正常行为。集合基于哈希表实现,遍历顺序由哈希值决定,不记录插入先后。Python 还会给字符串哈希加随机种子,所以同一段代码多次运行,字符串列表去重后的顺序都可能不一样。要保留顺序就改用 dict.fromkeys()。

Q:list(set(...)) 和 dict.fromkeys() 哪个更快?

A:两者平均时间复杂度都是 O(n),实际差别很小,通常远不如"要不要保序"这个决策重要。真正影响性能的是元素数量和数据是否可哈希:数据量很小或者元素不可哈希时,讨论这两种写法的快慢没有意义,先保证结果顺序正确。

Q:列表里装的是字典,怎么按某个字段去重?

A:字典不可哈希,不能直接进 set()。做法是先把每条记录里用来判重的字段取出来,再用循环配合一个记录该字段的集合来筛,或者把判重字段先转成可哈希的类型(比如把字典的相关字段拼成元组)。要注意判重字段相等时,你想保留第一次出现的记录还是最后一次,这决定了判断和覆盖的顺序。

Q:去重之后原来的列表变了吗?

A:没变。本文所有方法都是生成并返回一个新列表,原列表保持原样。如果你确实想让原列表变量指向去重后的结果,用切片赋值 nums[:] = list(dict.fromkeys(nums)) 可以做到真正原地替换内容,这样其他引用了同一个列表对象的地方也会看到变化。

0 人点赞