前言
Python 的内置类型看起来不多,但真到选型时,很多人还是靠感觉:反正 list 能放东西,dict 也能放东西,那就随便挑一个。结果代码能跑,却在几处细节上悄悄出问题——比如拿 list 当字典的键、拿 tuple 期待它「可变」、把字符串当字符数组去改。
要避免这些,得先抓住几个共同点:它们都是对象,都支持len()、in、迭代,都遵循统一的类型协议。再抓住几个差异点:可变还是不可变、有没有顺序、能不能哈希、允许不允许重复、能不能按下标访问。这几个属性的组合,基本决定了某个容器适合什么、不适合什么。
本文用一张大表把差异摊开,再逐个解释这些属性在实际编码中意味着什么,最后给出选型指南。示例均为 Python 3 写法。顺带说一句,Python 2 已于 2020 年 1 月 1 日停止维护,它独有的unicode、basestring、long类型在 Python 3 里都不存在,统一成了str和int。
一、一张大表横向对比
| 类型 | 可变 | 有序 | 可哈希 | 允许重复 | 下标访问 | 典型用途 |
|---|
| int | 否 | 不适用 | 是 | 不适用 | 不适用 | 计数、编号 |
| float | 否 | 不适用 | 是 | 不适用 | 不适用 | 测量值、比例 |
| bool | 否 | 不适用 | 是 | 不适用 | 不适用 | 标志位 |
| str | 否 | 是 | 是 | 是 | 是 | 文本 |
| bytes | 否 | 是 | 是 | 是 | 是 | 二进制数据 |
| bytearray | 是 | 是 | 否 | 是 | 是 | 可变的二进制缓冲 |
| list | 是 | 是 | 否 | 是 | 是 | 有序集合、待处理队列 |
| tuple | 否 | 是 | 通常情况下 | 是 | 是 | 定长记录、字典的键 |
| range | 否 | 是 | 是 | 否 | 是 | 惰性数字序列 |
| dict | 是 | 插入序 | 否 | 键唯一 | 按 key | 按名查找、分组 |
| set | 是 | 无序 | 否 | 否 | 不适用 | 去重、集合运算 |
| frozenset | 否 | 无序 | 是 | 否 | 不适用 | 需要可哈希的集合 |
表里有几个「不适用」值得说明:数字类型没有「顺序」和「下标」的概念,因为它们不是容器;dict 和 set 没有整数下标,只能按 key 或按哈希定位。
二、四个关键属性的含义
可变性(mutability):可变对象创建后能改内容,不可变对象不能。判断标准是「能不能不新建对象就改变值」。lst.append(1)不新建对象,所以 list 可变;s + "a"会新建字符串,所以 str 不可变。不可变带来的好处是安全——对象一旦造好就不会被别的代码偷偷改掉,适合当字典的键、适合当函数默认值。代价是每次「修改」都要造新对象。
有序性(order):这里的「有序」指有没有稳定的位置概念。str、list、tuple 有下标,是真正有序的;set 和 frozenset 无序,遍历顺序不保证;dict 从 Python 3.7 起保证按插入顺序遍历(3.6 里这个行为是 CPython 的实现细节,不要依赖)。注意 dict 的「保序」指的是插入顺序,不是排序——它不会因为键是数字就自动从小到大排。
可哈希性(hashability):一个对象可哈希,意味着它有一个在整个生命周期内不变的哈希值,并且能与其他对象比较相等。可哈希的对象才能当 dict 的键、放进 set。经验规则是:可变容器一律不可哈希。list、dict、set 都不能当键,tuple 和 frozenset 可以——但前提是它们里面的元素也都可哈希。
能否重复:list、tuple、str 允许重复;set、frozenset 不允许;dict 的值可以重复,键不能。
三、这些差异带来的实际后果
第一,能不能当字典的键。下面的例子说明规则怎么用。
# 适用于 Python 3.8+
d = {}
d[(1, 2)] = "点 A" # tuple 可哈希,可以当键
d[frozenset({1, 2})] = "集合" # frozenset 可哈希,可以
print(d[(1, 2)]) # 点 A
# d[[1, 2]] = "x" # TypeError: unhashable type: 'list'
# d[{1, 2}] = "x" # TypeError: unhashable type: 'set'
# 含可变元素的 tuple 也不可哈希
# d[(1, [2, 3])] = "x" # TypeError: unhashable type: 'list'第二,哈希相等的对象在集合里只算一个。这是很多「去重去不干净」或「数量对不上」问题的来源。
# 适用于 Python 3.8+
s = {1, 1.0, True}
print(len(s)) # 1,三者在哈希和相等判断上视为同一个
print(s == {1}) # True
# 字符串和数字是不同类型,不会互相顶掉
t = {1, "1"}
print(len(t)) # 2第三,「不可变」和「不可变容器里的内容」是两回事。tuple 本身不可变,但如果它装着 list,那个 list 是可以改的。语言规范的说法是:不可变容器保证的是「直接包含的引用」不变,不保证这些引用指向的对象不变。
# 适用于 Python 3.8+
t = (1, [2, 3])
t[1].append(4) # 合法:改的是里面的 list
print(t) # (1, [2, 3, 4])
# t[1] = [9] # TypeError:不能给 tuple 的元素重新赋值第四,不同类型之间不能随便比较大小。Python 3 里1 < "a"直接抛TypeError(Python 2 里不同类型的比较是按类型名排序的,这也是 2 和 3 的一大差异)。
四、怎么选:一张决策表
| 你的需求 | 选它 | 理由 |
|---|
| 一条固定不变的记录 | tuple | 不可变、可解包、可哈希 |
| 需要按名字取字段的记录 | collections.namedtuple 或 dataclass | 可读性 |
| 会不断增删的有序集合 | list | 尾部增删均摊 O(1) |
| 只读的有序集合 | tuple | 语义明确,防止误改 |
| 按 key 找值 | dict | 平均 O(1) 查找 |
| 去重 | set | 插入即去重 |
| 需要能当键的集合 | frozenset | 不可变所以可哈希 |
| 一段数字区间 | range | 惰性,不占内存 |
| 需要改的字节缓冲 | bytearray | 原地修改 |
| 二进制数据只读 | bytes | 不可变、可哈希 |
一个实用的心法:先问「要不要改」,再问「要不要按名字查」,最后问「要不要唯一」。三个问题的答案组合起来,容器基本就定了。
常见坑点
- 以为「不可变的 tuple」里面的东西也改不了。
❌ 把一条含列表的记录塞进 tuple,以为整条就此冻结。
✅ tuple 只保证元素引用不变;要真正不可变,里面的元素也得是不可变对象,比如把内层 list 换成 tuple。
- 把 str 当成可以就地修改的字符数组。
❌s[0] = "H"—— 抛TypeError: 'str' object does not support item assignment。
✅ 字符串不可变,用切片或replace造新串;需要频繁改字符时先转成list再"".join回去。
- 用 list 或 set 当字典的键。
❌d[[1, 2]] = "x"抛TypeError: unhashable type: 'list'。
✅ 转成tuple或frozenset再当键。
- 忽略了
1、1.0、True在集合里是同一个人。
❌ 用集合统计「不同取值有多少种」时,混入了布尔值和数值,数量比预期少。
✅ 统计前统一类型,或改用「类型 + 值」的元组当元素:{(type(x).__name__, x) for x in data}。
- 以为 set 会保持某种顺序。
❌ 依赖set的遍历顺序做输出,换个数据就变了。
✅ 输出前显式排序:sorted(s);需要保序去重用dict.fromkeys。
- 把 range 当成真的列表用。
❌range(3).append(5)—— range 没有append,它不可变。
✅ range 只用来生成序列,需要可变就list(range(3))。
- 不同类型之间比较大小。
❌1 < "a"在 Python 3 里抛TypeError(Python 2 里能比较,属于老代码迁移时的隐藏坑)。
✅ 比较前统一类型,或先判断类型再决定比较方式。
- 用
==比较浮点数。
❌0.1 + 0.2 == 0.3结果是False,浮点二进制表示无法精确表达这些十进制小数。
✅ 用math.isclose(a, b)做近似比较,或改用decimal.Decimal处理需要精确十进制运算的场景。
总结
| 属性 | 可变的代表 | 不可变的代表 |
|---|
| 线性序列 | list、bytearray | str、bytes、tuple |
| 键值映射 | dict | 无内置不可变 dict |
| 集合 | set | frozenset |
| 数字 | 无 | int、float、bool(均不可变) |
记住三条判据就够了:可变的东西不能当键;有序只保证位置稳定,不代表会排序;不可变容器的不可变性只作用到直接子元素。选型时先定可变性,再定查法,最后定唯一性,比死记硬背每张表都管用。