在Python的世界里,我们每天都在和 list
、tuple
、dict
、set
打交道。但你有没有想过:
set
真的是“无序”的吗?这些看似基础的问题,其实都源于Python对数据类型的底层设计。接下来,我们就来深入剖析Python中常见数据类型的本质,从“序列”、“容器”这些抽象概念讲起,到 list
与 tuple
、dict
与 set
的对比实践,以及最后的相关的底层实现原理。
刚开始学Python时,你可能听过这些术语:list
是可变序列”、“str
是扁平序列”、“dict
是容器类型……听起很玄乎,其实他们只是从不同视角对数据结构的分类,我们来一一拆解。
提到 “序列”,你可以先联想日常生活中的 “排队”—— 每个人(数据)按顺序站好,有自己的位置(索引),这就是序列的核心特征。
什么是序列?
广义上,序列是一种连续存储的数据格式,就像把书按顺序摆放在书架的同一层,每个数据都有固定的 “位置编号”,通过编号能快速找到对应数据。简单说,序列就是一组按照顺序排列的数据集合。
什么是Python中的序列类型?
Python对序列做了一层抽象封装,形成了“序列类型”,只要某个数据类型支持一下操作,就可以归为序列类型:
seq[0]
seq[1:3]
seq1 + seq2
、seq * 3
x in seq
Python 中常见的序列类型有:list
(列表)、tuple
(元组)、str
(字符串)、bytes
(字节串)、array
(数组)。
1 | # 序列通用操作示例 |
序列类型的两种分类方式
类型 | 特点 | 示例 |
---|---|---|
可变序列(MutableSequence) | 可以动态增删改 | list , bytearray , array.array |
不可变序列(Sequence) | 一旦创建就不能修改,任何“修改”操作都会返回新对象 | tuple , str , bytes |
Tips:
tuple
虽然是不可变,但如果它内部包含list
,你依然可以修改那个list
,因为tuple
只保证“引用不变”,不保证“内容不变”。
类型 | 存储内容 | 特点 | 示例 |
---|---|---|---|
容器序列 | 存储对象的引用 | 可以嵌套任意类型 | list , tuple |
扁平序列 | 存储对象的值本身 | 更紧凑,效率更高 | str , bytes , array |
举个例子:
['a', 'b', 'c']
中每个元素都是对字符串对象的引用;而'abc'
是连续的字符值存储在内存中,更节省空间。
数值类型是Python中最基础的数据类型之一,专门用来表示数字,主要包括三类:
10
、-5
、0
,Python 的 int 没有大小限制,能存非常大的数(比如 10**100
这样的 “天文数字”)。True
(等价于 1)和 False
(等价于 0)两个值,常用于条件判断。3.14
、-0.5
,注意浮点数有精度限制(比如 0.1 + 0.2
不等于 0.3
)。a + bj
的数字(j
是虚数单位),比如 2 + 3j
,主要用于科学计算场景。需要注意的是,它们不属于“容器”,因为不包含其他对象,是原子性的。
这里的 “容器类型” 和前面提到的 “容器序列” 不一样 —— 它是从 “功能” 角度分类,指所有能 “容纳其他对象” 的数据结构。简单说,只要一个数据类型能把多个对象 “打包” 在一起,就是容器类型。
容器序列:特指 list
、tuple
这类支持索引的序列。
容器类型:泛指能容纳其他对象的数据结构,比如 dict
、set
、list
。
Python 中最常用的容器类型有 4 个:
接下来我们就聚焦这 4 个核心容器类型,从语法、原理、性能到场景,做一次全方位对比。
1 | # list: 可变,灵活 |
特性 | list | tuple |
---|---|---|
是否可变 | 是 | 否 |
是否支持增删改 | 是 | 否 |
是否支持索引/负数索引/切片 | 是 | 是 |
内存占用 | 较大 | 较小 |
创建速度 | 慢 | 快 |
是否相互转换 | tuple() | list() |
Python的list
实际上是一个动态扩容顺序表,采用“分离式结构”:
扩容策略:
这种“过度分配”(over-allocation)策略保证了 append()
操作的均摊时间复杂度为 O(1)。
1 | demo_list = [] |
tuple
使用“一体式结构”的顺序表,创建后大小固定,不能扩容。
正因为不可变,Python 可以对小元组进行缓存。比如 (1,2,3)
第一次创建后会被缓存,下次再创建相同的元组,直接复用内存,极大提升性能。
使用timeit
模块进行测试:
1 | # 创建 速度对比 |
结论:tuple的创建速度更优,访问速度略快。其原因在于:
场景 | 推荐类型 |
---|---|
存储固定数据(如坐标、配置) | tuple |
函数返回多个值 | tuple (return x, y ) |
需要频繁增删改的数据 | list |
用作字典的键 | tuple (不可变) |
作为集合元素 | tuple |
1 | # 1. 返回经纬度坐标 |
1 | # 1. 记录用户一周内看过的帖子ID |
list操作:
index()
:返回指定元素下标count()
:统计元素出现次数len()
:获取列表长度append()
:末尾追加元素extend()
:扩展列表(添加序列中的所有元素)insert()
:指定位置插入元素pop()
:删除并返回指定位置元素(默认最后)remove()
:移除第一个匹配项clear()
:清空列表reverse()
和sort()
:原地反转和排序tuple操作:
index()
:查找元素位置count()
:统计元素出现次数len()
:获取元组长度通用函数:
reversed()
:返回反转后的迭代器sorted()
:返回排序后的新列表注意:如果想给元组排序 / 反转,可以用全局函数 sorted()
和 reversed()
,它们会返回新列表 / 迭代器,不修改原元组:
1 | t = (3,1,2) |
1 | # 字典:键值对 |
{}
或 dict()
,空集合只能用 set()
—— 因为 {}
优先被解析为字典。特性 | Dict(字典) | Set(集合) |
---|---|---|
存储结构 | 键值对(key: value) | 单个元素(无键值) |
元素唯一性 | key 唯一,value 可重复 | 所有元素唯一(自动去重) |
有序性 | 3.7+ 有序(插入顺序) | 无序(无法通过索引访问) |
索引访问 | 通过 key 访问,如 d['key'] |
无序,无索引 |
核心用途 | 高效查询(通过 key 找 value) | 去重、集合运 |
时间复杂度(查/增/删) | O(1) | O(1) |
老版本结构(紧凑但低效):
1 | +-------------------------------+ |
新版本结构(稀疏索引 + 紧凑存储):
1 | Indices: [None, 0, None, None, 1, ...] |
优势:节省内存、提升遍历速度、支持有序迭代。
dict
和 set
的核心都是哈希表(Hash Table),通过哈希函数将键映射到数组索引,实现近乎常数时间的查找。Python采用优化后的哈希表结构,提高了内存利用率和访问效率。
dict
的哈希表结构:存储“哈希值(hash)、键(key)、值(value)”三个元素,支持快速键值查找1 | # 索引区(记录元素在数据区的位置) |
set
只存储 “哈希值(hash)、元素(element)”,因为没有 value,结构更简单, 专注于快速成员检测1 | Entries: |
操作分析:
什么是哈希冲突?
两个不同的键,计算出相同的哈希值(或索引),就会发生冲突。
Python采用开放寻址法解决冲突:如果位置被占,就找下一个空位。这可能会降低操作效率。为了保证高效性,哈希表始终保持至少1/3的剩余空间,当空间不足时自动扩容。
如何实现扩容机制
当哈希表使用率 > 2/3 时,触发扩容。
扩容为原来的 2~4 倍,并重新哈希所有元素。
这也是为什么
dict
插入操作虽然是 O(1),但偶尔会“卡一下”——那是它在扩容。
1 | # 1. 字典推导式 |
1 | # 1. 配置管理 |
1 | # 1. 集合运算 |
1 | # 1. 权限管理 |
dict 操作:
keys()
:返回所有键values()
:返回所有值items()
:返回所有键值对del
:删除键值对clear()
:清空字典for key in dict
或for key, value in dict.items()
set 操作:
add()
:添加元素update()
:添加多个元素(传入序列)remove()
:删除指定元素(不存在则报错)discard()
:删除指定元素(不存在不报错)pop()
:随机删除并返回一个元素(因集合无序需谨慎使用)1 | # 错误做法:使用列表查找 |
1 | # 需要修改数据:用列表 |
1 | # 使用__sizeof__()分析内存占用 |
场景 | 推荐数据结构 | 原因 |
---|---|---|
动态数据集合 | 列表(list) | 支持频繁增删改 |
固定数据集合 | 元组(tuple) | 更快的创建和访问速度 |
键值映射 | 字典(dict) | 快速的键值查找 |
唯一值集合 | 集合(set) | 快速成员检测和去重 |
配置信息 | 元组/字典 | 根据是否需要修改选择 |
数据缓存 | 字典 | 快速的键值访问 |
记住这几个原则:
理解了这些数据结构的底层原理和特性,能够帮助我们在实际开发中做出更合理的选择,编写出更高效、更优雅的Python代码。