跳到正文

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 订正遍历删除示例

把一门技术讲透到既懂也会用