学习路径:Python_AIGC · Day 3
知识点简介
Python 四大内置容器(list、dict、set、tuple)几乎出现在每一段 Python 代码中。但很多人只用了它们 20% 的功能。
今天不讲「list 怎么追加元素」这种基础,而是从 底层实现 → 高级操作 → 性能对比 → 避坑指南 的路径走一遍,让你真正理解每个容器的「性格」。
1️⃣ list --- 动态数组
底层实现
Python list 底层是 动态数组(Dynamic Array),不是链表:
css
底层结构示意:
[obj1][obj2][obj3][obj4][ 空 ][ 空 ]
↑ ↑
已占用(4个) 预留容量
- 尾部追加
O(1)摊还 ------ 因为空间不够时自动扩容(约 1.125 倍) - 中间插入/删除
O(n)------ 需要挪动后续元素 - 随机访问
O(1)------ 直接通过索引计算内存地址
高级操作
python
# 切片赋值(批量替换)
nums = [1, 2, 3, 4, 5]
nums[1:4] = [20, 30] # → [1, 20, 30, 5]
nums[1:1] = [1.5] # 插入 → [1, 1.5, 20, 30, 5]
# 列表推导 + 条件过滤
squares = [x**2 for x in range(10) if x % 2 == 0] # [0, 4, 16, 36, 64]
# 嵌套推导展平
matrix = [[1, 2], [3, 4], [5, 6]]
flat = [x for row in matrix for x in row] # [1, 2, 3, 4, 5, 6]
# bisect 二分查找(有序列表)
import bisect
scores = [60, 70, 80, 90]
pos = bisect.bisect_left(scores, 75) # → 2(插入位置)
bisect.insort(scores, 75) # 插入并保持有序
# `*` 解包操作符
first, *rest = [1, 2, 3, 4, 5] # first=1, rest=[2,3,4,5]
避坑指南
python
# ⚠️ 复制陷阱
a = [[1], [2], [3]]
b = a * 3 # 浅拷贝!内部列表是同一引用
b[0][0] = 999
print(a) # [[999], [2], [3]] --- 被改了!
# ✅ 正确复制
import copy
c = copy.deepcopy(a)
# ⚠️ 循环中删除
nums = [1, 2, 3, 4, 5]
for n in nums:
if n % 2 == 0:
nums.remove(n) # 索引偏移,漏删!
# ✅ 正确做法:倒序遍历或列表推导
nums[:] = [n for n in nums if n % 2 != 0]
2️⃣ dict --- 哈希表
底层实现
Python 3.7+ 的 dict 是 有序哈希表(Compact Hash Table):
- 键通过
hash()计算哈希值,取模定位 - 冲突解决:开放地址法(Open Addressing)
- 插入顺序被保留(3.7+ 语言特性,之前也有 CPython 实现)
- 3.6+ 引入
PyDictKeysObject,分离键索引和值数组,内存减少 20-30%
高级操作
python
# setdefault --- 一行搞定「取/设默认值」
data = {}
users = data.setdefault('users', [])
users.append('张三')
# data → {'users': ['张三']}
# defaultdict --- 自动工厂
from collections import defaultdict
groups = defaultdict(list)
groups['dev'].append('张三') # 自动创建空列表
groups['dev'].append('李四')
# dict 推导 + 过滤
squares = {x: x**2 for x in range(10) if x > 5}
# {6: 36, 7: 49, 8: 64, 9: 81}
# 「|」合并操作符(3.9+)
config = {'host': 'localhost', 'port': 8080}
override = {'port': 9000, 'debug': True}
merged = config | override # {'host': 'localhost', 'port': 9000, 'debug': True}
# 解包合并
merged = {**config, **override}
# get() 的巧妙用法
user = users.get('admin') or 'default_admin' # 取到 None 时给默认
# 缺失键触发
d = {}
d.setdefault('count', 0)
d['count'] += 1 # 安全,无需先判断
避坑指南
python
# ⚠️ 不可哈希的键
# d = {[1, 2]: 'value'} ← TypeError: unhashable type: 'list'
# 解决方案:用 tuple 或 frozenset
# ⚠️ 循环中修改
d = {'a': 1, 'b': 2, 'c': 3}
# for k in d: # RuntimeError: dictionary changed size during iteration
# if k == 'b':
# del d[k]
# ✅ 安全删除方式
for k in list(d.keys()): # 快照迭代
if k == 'b':
del d[k]
# ⚠️ 自定义对象做键
class User:
def __init__(self, name):
self.name = name
# 必须实现 __hash__ 和 __eq__
u1, u2 = User('张三'), User('张三')
d = {u1: 'first'}
print(d.get(u2)) # None --- 两个对象不相等
3️⃣ set --- 无序集合
底层实现
set 的底层也是哈希表,和 dict 的键实现一样:
- 元素必须可哈希
- 查
O(1),增O(1)摊还 - 不保证顺序(但 CPython 实现中插入顺序有规律,不要依赖!)
高级操作
python
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
a | b # 并集 → {1, 2, 3, 4, 5, 6}
a & b # 交集 → {3, 4}
a - b # 差集 → {1, 2}
a ^ b # 对称差 → {1, 2, 5, 6}
# 超集/子集判断
{1, 2}.issubset({1, 2, 3}) # True
{1, 2, 3}.issuperset({1, 2}) # True
{1, 2}.isdisjoint({3, 4}) # True(无交集)
# 去重神器
names = ['张三', '李四', '张三', '王五']
unique = list(set(names)) # ['张三', '李四', '王五']
# ⚠️ 但会丢失顺序!要保留顺序用:
# 保留顺序的去重
seen = set()
ordered = []
for name in names:
if name not in seen:
seen.add(name)
ordered.append(name)
# frozenset --- 不可变集合(可以用作 dict 的键)
frozen = frozenset([1, 2, 3])
d = {frozen: 'hashable'}
避坑指南
python
# ⚠️ 空集合不是 {}
empty_set = {} # 这是 dict!
empty_set = set() # ✅ 正确的空集合
# ⚠️ 集合推导是 {}
squares = {x**2 for x in range(5)} # {0, 1, 4, 9, 16} ← set
4️⃣ tuple --- 不可变序列
底层实现
- 底层也是数组,但不可变
- 因为不可变,可以哈希(元素也是可哈希的)
- 创建后不能增删改,遍历更快,内存更省
高级操作
python
# 命名元组 --- 轻量级数据类
from collections import namedtuple
Point = namedtuple('Point', ['x', 'y'])
p = Point(10, 20)
p.x # → 10
p.y # → 20
x, y = p # 解包
# 带类型注解的命名元组(3.6+)
from typing import NamedTuple
class Employee(NamedTuple):
name: str
age: int
department: str
e = Employee('张三', 28, '技术部')
e.name # '张三'
# 单元素元组的逗号
single = (42,) # ✅ tuple
not_tuple = (42) # 这只是 int!
# `*` 元素解包
first, *middle, last = (1, 2, 3, 4, 5)
# first=1, middle=[2,3,4], last=5
避坑指南
python
# ⚠️ 元组的不可变是浅层的
t = ([1, 2], [3, 4])
t[0].append(999) # 可以!t 变成 ([1,2,999], [3,4])
# t[0] = [] # ❌ 不可以!会报 TypeError
# ⚠️ 命名元组是元组的子类,但不是数据类的替代
# 如果字段多且需要校验,用 @dataclass
🏆 性能对比速查
| 操作 | list | dict | set | tuple |
|---|---|---|---|---|
| 索引/查找 | O(1) | O(1) | --- | O(1) |
| 包含检查 | O(n) | O(1) | O(1) | O(n) |
| 尾部插入 | O(1)‡ | O(1)‡ | O(1)‡ | ❌ |
| 任意插入 | O(n) | O(1) | --- | ❌ |
| 删除 | O(n) | O(1) | O(1) | ❌ |
| 内存 | 少 | 多 | 多 | 最少 |
| 可哈希 | ❌ | 仅键 | ❌ | ✅ |
‡ 摊还时间复杂度
📌 要点总结
| 容器 | 核心特性 | 最适合的场景 |
|---|---|---|
| list | 有序、可重复、可变 | 按索引访问、顺序遍历、栈/队列 |
| dict | 键值对、有序(3.7+)、键唯一 | 映射/缓存/配置/分组 |
| set | 无序、元素唯一、数学运算 | 去重/交集/差集/成员判断 |
| tuple | 有序、不可变、可哈希 | 函数多返回值/字典键/不可变数据 |
- 判断成员存在 :
set/dictkeys 远快于 list - 需要顺序:用 list 或 dict(3.7+ 有序)
- 需要不可变:用 tuple,还能做 dict 的键
- 优先用推导式:可读性更高、通常更快
- 不要依赖集合顺序:不同 Python 实现可能不同