主题
03 容器与序列
一句话:list 用连续数组换按下标访问,dict/set 用哈希表换 O(1) 成员判断;真正的分水岭不是语法,而是「可变对象赋值即共享引用」。
适用:CPython 3.11+。dict 保持插入顺序是 3.7+ 的语言保证;dict 合并运算符 | 需要 3.9+。
为什么需要
数据处理的第一步是把一批值放进合适的容器。选错容器,复杂度会差一个量级:在 list 里反复做 x in data 是 O(n),在 set 里是平均 O(1);需要稳定顺序和下标时用 list,需要键值映射时用 dict。更隐蔽的是可变性:b = a 对 list 是共享引用,改 b 会连带改 a;浅拷贝只复制一层,嵌套结构仍共享。这两类问题在数据清洗和配置复制里出现频率最高。
核心机制
四种容器怎么选
| 容器 | 有序 | 可变 | 元素唯一 | 查找 | 典型用途 |
|---|---|---|---|---|---|
list | 是 | 是 | 否 | 下标 O(1),值 O(n) | 有序批数据、栈/队列 |
tuple | 是 | 否 | 否 | 下标 O(1) | 固定结构、多返回值、dict 键 |
dict | 插入序 | 是 | 键唯一 | 平均 O(1) | 映射、配置、计数 |
set | 否 | 是 | 是 | 平均 O(1) | 去重、交并差、成员判断 |
list 是动态数组,尾部 append 摊还 O(1),中间插入/删除 O(n)。dict/set 是哈希表,键必须可哈希;list、dict、set 不能当键,tuple 可以(前提是元素也可哈希)。
list:遍历时删除会漏元素
先看一个可复现的对比:
python
nums = [1, 2, 4, 6, 8]
for x in nums:
if x % 2 == 0:
nums.remove(x)
print(nums) # [1, 4, 8]:4 和 8 没有被删掉
nums = [1, 2, 4, 6, 8]
nums[:] = [x for x in nums if x % 2 == 1]
print(nums) # [1]:过滤重建,结果正确原因:remove 删除元素后,后面的元素左移一位,但迭代器内部下标继续加一,于是跳过了紧邻的下一个元素。遍历副本或过滤重建都能避免:
python
for x in nums.copy():
if x % 2 == 0:
nums.remove(x)其他高频行为:sort() 原地排序并返回 None,sorted() 返回新列表;lst = lst.sort() 是经典错误。append 加一个元素,extend 展开一个可迭代对象,两者不要混。
tuple:不可变带来的可哈希
python
t = (1, 2, 3)
single = (1,) # (1) 只是整数 1
d = {(1, 2): "point"} # tuple 可作键
# d = {[1, 2]: "x"} # TypeError: unhashable type: 'list'
x, y = (3, 4)
first, *rest, last = [1, 2, 3, 4]
print(x, y, first, rest, last) # 3 4 1 [2, 3] 4命名元组用 collections.namedtuple 或 typing.NamedTuple;需要默认值和类型时优先 dataclass(05)。
dict:遍历时不能改大小
python
d = {"a": 1, "b": 2}
# for k in d:
# d.pop(k) # RuntimeError: dictionary changed size during iteration
for k in list(d): # 先复制键列表
d.pop(k)
print(d) # {}读取优先 d.get(k, default),避免 KeyError;但要注意默认值只在键不存在时生效,这和 or 不同(02)。计数用 collections.Counter,分组用 defaultdict(list)。合并用 a | b(3.9+,返回新字典)或 a.update(b)(原地)。
set:去重与集合运算
python
a, b = {1, 2, 3}, {3, 4}
print(a & b, a | b, a - b, a ^ b) # {3} {1, 2, 3, 4} {1, 2} {1, 2, 4}
print(list(dict.fromkeys([1, 2, 1, 3]))) # [1, 2, 3]:需要保序去重时set 无序,不要依赖迭代顺序;需要「去重且保序」时用 dict.fromkeys。
可变性、浅拷贝与深拷贝
python
a = [[1, 2], [3, 4]]
b = a.copy() # 浅拷贝:外层新列表,内层仍共享
b[0][0] = 99
print(a) # [[99, 2], [3, 4]]
import copy
c = copy.deepcopy(a)
c[0][0] = 0
print(a[0][0]) # 99:深拷贝完全独立规则:int/float/str/tuple 不可变;list/dict/set 可变。不可变对象「修改」总是产生新对象;可变对象赋值、切片、浅拷贝都可能共享底层数据。
推导式:表达「要什么」
python
squares = [x * x for x in range(5)] # [0, 1, 4, 9, 16]
evens = [x for x in range(10) if x % 2 == 0] # [0, 2, 4, 6, 8]
pairs = [(i, j) for i in range(2) for j in range(2)]
by_len = {s: len(s) for s in ["a", "bb"]} # {'a': 1, 'bb': 2}
chars = {c for c in "abracadabra"} # 自动去重超过一个 if 或多个 for 时,拆成普通循环或函数;只为了副作用(打印、写文件)不要用推导式。
最小上手
保存为 containers.py 后运行,预期输出 [1] 和 True:
python
nums = [1, 2, 4, 6, 8]
nums[:] = [x for x in nums if x % 2 == 1]
print(nums) # [1]
a = [[1, 2]]
b = a.copy()
b[0][0] = 9
print(a[0][0] == 9) # True:浅拷贝共享内层生产动作
- 成员判断:热路径上的
x in list改成set或dict;先确认元素可哈希。 - 遍历修改:删除/新增容器元素时遍历副本,或直接重建;dict 改大小一定报
RuntimeError。 - 复制语义:配置复制先明确要浅还是深;嵌套结构用
copy.deepcopy,但注意开销。 - 排障三段:
- 现象:结果少了元素 → 命令:在循环里打印
id(nums)、下标和当前元素 → 判断:是否边遍历边删除导致下标跳跃。 - 现象:
TypeError: unhashable type→ 命令:type(key)、hash(key)→ 判断:是否把 list/dict 放进了 set 或当 dict 键。 - 现象:改副本影响了原对象 → 命令:
id(a[0]) == id(b[0])→ 判断:是否只做了浅拷贝。
- 现象:结果少了元素 → 命令:在循环里打印
边界
- list 适合有序和小规模;百万级数值计算交给 NumPy(10),不要用 list 硬扛。
- dict/set 的 O(1) 是平均复杂度;哈希冲突严重或元素不可哈希时退化。
- tuple 的不可变是「引用不可变」,如果元素本身是 list,内部仍可改。
讲清楚
list、tuple、dict、set 的分工由访问模式决定:要顺序和下标用 list,要固定结构用 tuple,要键值映射用 dict,要成员判断和去重用 set。真正的坑在可变性:赋值、切片、浅拷贝对可变对象都是共享引用,改一个会连带改另一个;只有 deepcopy 才完全独立。遍历 list 时删除会因左移跳过元素,正确做法是遍历副本或过滤重建;遍历 dict 时改大小会直接报错。选容器先看复杂度和是否需要顺序,再看复制语义。
适用 CPython 3.11+ | 初稿 2026-08,2026-10 订正遍历删除示例