Python set去重与dict.fromkeys去重的性能对比

周予安
周予安
发布时间:2026-10-10| 2人浏览过
原创

在Python中去除列表重复元素时,最常用的两种内置方法是 list(set(seq)) 和 list(dict.fromkeys(seq))。前者凭借高度优化的集合哈希表拥有极高的执行效率,但会打乱原有元素的相对顺序;后者利用自Python 3.7起正式写入语言规范的字典键保序特性,在保留原序列顺序的前提下完成去重。本文通过底层原理解析、基准耗时测试与内存分配对比,呈现两者的性能差异及边界条件。

一、去重机制与核心语法差异

去除可迭代对象中的重复元素,其核心逻辑均依赖哈希表(Hash Table)对元素的唯一性校验。两者的根本区别在于底层使用的数据结构细节以及是否维护键的插入顺序。

Python set去重与dict.fromkeys去重的性能对比
图1:set去重与dict.fromkeys去重的底层执行模型对比

具体用法如下示例所示:

raw_data = [3, 1, 2, 1, 3, 5, 2]

# 1. 使用 set 去重(不保证原顺序)
dedup_set = list(set(raw_data))
# 示例输出:[1, 2, 3, 5]

# 2. 使用 dict.fromkeys 去重(保持首次出现的顺序)
dedup_dict = list(dict.fromkeys(raw_data))
# 示例输出:[3, 1, 2, 5]

在底层实现中,Python的 set 是单纯的哈希表,每个表项存储哈希值和对象指针;而 dict 采用分体式紧凑哈希表设计,包含一个稀疏散列表和一个按插入顺序追加记录的紧凑数组。dict.fromkeys 会将每个元素作为键,默认对应值为 None。这意味着 dict.fromkeys 不仅需要写入键,还需要为所有键填充默认值指针,带来微小的额外操作。

二、性能测试方案与操作步骤

为了科学对比两者的耗时表现,我们使用Python内置的 timeit 模块进行多量级基准测试。测试覆盖小规模(1,000项)、中规模(100,000项)以及大规模(1,000,000项)数据,并分别测试“低重复率(90%唯一项)”与“高重复率(10%唯一项)”场景。

Python set去重与dict.fromkeys去重的性能对比
图2:性能基准评估实验与操作流程

完整的基准性能测量脚本如下:

import timeit
import random

# 生成测试数据:100万整数,数值范围0~10万(产生大量重复值)
test_data = random.choices(range(100_000), k=1_000_000)

def test_set(data):
    return list(set(data))

def test_dict(data):
    return list(dict.fromkeys(data))

# 单次执行预热以确保字节码编译及环境稳定
_ = test_set(test_data[:10])
_ = test_dict(test_data[:10])

# 执行10次,测量总运行时间
n = 10
time_set = timeit.timeit(lambda: test_set(test_data), number=n)
time_dict = timeit.timeit(lambda: test_dict(test_data), number=n)

print(f"数据量: 1,000,000 项,执行次数: {n}")
print(f"set去重耗时: {time_set / n:.4f} 秒/次")
print(f"dict.fromkeys去重耗时: {time_dict / n:.4f} 秒/次")
print(f"性能比 (dict / set): {time_dict / time_set:.2f}x")

三、测试结果与性能多维分析

以下为在 Python 3.11(x86_64, Linux)环境下的基准测试参考结果(各组数据执行多次后取平均耗时):

数据规模与重复度 list(set(seq)) 耗时 list(dict.fromkeys(seq)) 耗时 耗时比 (dict/set) 结果顺序
1,000 项(中等重复) 14.2 μs 19.8 μs 约 1.39x set乱序 / dict保序
100,000 项(中等重复) 1.82 ms 2.51 ms 约 1.38x set乱序 / dict保序
1,000,000 项(高重复,90%重复) 16.8 ms 22.4 ms 约 1.33x set乱序 / dict保序
1,000,000 项(全唯一,0%重复) 38.5 ms 52.3 ms 约 1.36x set乱序 / dict保序
Python set去重与dict.fromkeys去重的性能对比
图3:百万级规模下两种去重方案耗时实测对比

耗时差距的核心成因

  • C层实现结构:set 在CPython内部结构极为单一,专用于维护无序唯一键;dict 必须兼顾映射语义,内部除了哈希索引表外,还需维持一份记录插入先后次序的条目数组(Entries Array)。
  • 额外引用开销:dict.fromkeys(seq) 会将每个键的值隐式指向 None。虽然指向的是同一个全局 Py_None 单例,但在C语言层面依然存在对键值对项的指针赋值与引用计数递增处理。
  • 转换路径耗时:在将字典转回列表时,list(dict) 需要从内部结构中提取键数组转换为新的 PyListObject;而 list(set) 直接遍历连续的哈希表有效单元并填充列表。

内存开销对比

去重过程涉及两个中间态对象:临时容器(set或dict)与最终生成的列表。通过 tracemalloc 分析可以发现:在元素唯一项较多(如100万个不重复整数)的极端场景下,dict 内部由于同时存储 (hash, key, value) 结构,其峰值内存比同等容量的 set 约高出 15%~25%。但若原始数据中重复率极高,由于最终去重后的尺寸很小,两者的内存差异在宏观上基本可以忽略。

四、实际开发中的选型策略

在工程实践中,选择去重方案不应仅看微秒级的速度差距,更应结合业务对“元素顺序”的要求及代码可读性进行综合权衡。

Python set去重与dict.fromkeys去重的性能对比
图4:去重选型决策路径与场景推荐
注意事项:不可哈希对象的处理边界
set 与 dict.fromkeys 均要求列表中的元素必须是可哈希对象(Hashable)。若列表中包含字典或子列表(如 [[1, 2], [1, 2]]),两者均会抛出 TypeError: unhashable type: 'list'。对于不可哈希元素的保序去重,需改用其他方案(如将子列表转换为元组后再处理,或通过辅助函数迭代)。

五、总结

综合性能基准与工程特性,两者的核心结论如下:

  1. 速度表现:在各数据量级与重复度下,list(set(seq)) 均保持领先,执行耗时通常仅为 list(dict.fromkeys(seq)) 的 70% 到 75%(即 dict.fromkeys 耗时高出约 30%~40%)。
  2. 保序代价:dict.fromkeys 牺牲了这部分微小的性能,换取了严格的元素插入保序能力。相比于早期 Python 版本中编写 for 循环结合 seen.add() 的保序方式,dict.fromkeys 全程在底层 C 代码中运行,依然是目前 Python 原生保序去重中性能最顶尖的写法。
  3. 选用准则:在无序场景中,优先采用语义明确、速度最快的 set;当业务要求维持序列原始先后关系时,果断采用 dict.fromkeys。
相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。