Python · 9 分钟阅读
列表、字典与元组
list / dict / tuple 是 Python 最常用的三种数据结构,它们分别对应 可变序列、哈希映射、不可变序列。理解三者的差异,能让你选型时不再凭感觉。
1. 一览
| 特性 | list |
dict |
tuple |
|---|---|---|---|
| 是否有序 | ✅(插入序) | ✅(插入序,3.7+) | ✅ |
| 是否可变 | ✅ | ✅ | ❌ |
| 重复元素 | 允许 | key 唯一 | 允许 |
| 索引访问 | O(1) | 通过 key,O(1) | O(1) |
| 内存开销 | 中 | 高(哈希表) | 低 |
| 可哈希 | ❌ | ❌ | ✅(内容都不可变时) |
| 常见用途 | 顺序集合 | 键值映射 | 不可变记录 / dict key |
2. 列表(list)
2.1 内存模型
Python 列表在 CPython 中被实现为 动态指针数组(over-allocated array of PyObject*):
- 列表本身 不 直接保存元素,而是保存一组 指针,每个指针指向堆上的真实对象(
int/str/list/ 自定义类…)。 - 这种设计让列表天然 异构:同一个
list可以同时有int、str、dict、自定义对象。 - 当元素数量超过当前容量时,CPython 会分配一个更大的数组并 把旧指针拷过去。为了减少扩容次数,分配策略采用了 几何级数(约 1.125× ~ 1.5×)。
Java 的
ArrayList和 C++ 的std::vector内部都用了类似的过度分配策略。
2.2 操作与复杂度
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
lst[i] |
O(1) | 索引读写 |
lst.append(x) |
O(1) 均摊 | 平均常数,最坏 O(n)(扩容) |
lst.pop() |
O(1) | 弹出末尾 |
lst.pop(0) / lst.insert(0, x) |
O(n) | 要搬动所有元素 |
lst.insert(i, x) |
O(n) | 同上 |
x in lst |
O(n) | 线性搜索 |
lst.remove(x) |
O(n) | 先查找再删除 |
lst.sort() |
O(n log n) | Timsort,原地排序,稳定 |
min(lst) / max(lst) / sum(lst) |
O(n) | 一次扫描 |
lst.reverse() |
O(n) | 原地反转 |
reversed(lst) |
O(n) | 惰性返回迭代器,不占额外空间 |
列表是 mutable、有序 的;这是它和
tuple/set的核心区别。
2.3 创建与基本操作
empty = [] # 空列表
nums = [1, 2, 3, 4, 5]
fruits = ["apple", "banana", "cherry"]
mixed = [1, "two", 3.0, [4, 5]] # 可以装任意类型
| 操作 | 说明 | 示例 |
|---|---|---|
len(xs) |
长度 | len([1,2,3]) → 3 |
xs[i] / xs[i] = x |
索引读写 | nums[0] = 10 |
xs.append(x) |
末尾追加 | fruits.append("orange") |
xs.insert(i, x) |
指定位置插入 | fruits.insert(1, "kiwi") |
xs.pop() / pop(i) |
弹出末尾 / 指定位置 | fruits.pop() → 'orange' |
xs.remove(x) |
删除 第一个 等于 x 的元素 | fruits.remove("banana") |
del xs[i] |
按索引删除 | del nums[0] |
xs.sort() |
原地排序(稳定 Timsort) | fruits.sort() |
sorted(xs) |
返回新列表 | sorted([3,1,2]) → [1,2,3] |
xs.reverse() |
原地反转 | nums.reverse() |
x in xs |
成员判断(O(n)) | "apple" in fruits |
2.4 列表推导式
squares = [x * x for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
nums = list(range(1, 13))
# 打印偶数
[print(x) for x in nums if x % 2 == 0]
# 2 4 6 8 10 12
# 转大写
words = ["hello", "world", "zen", "python"]
upper = [w.upper() for w in words]
print(upper) # ['HELLO', 'WORLD', 'ZEN', 'PYTHON']
# 展平二维矩阵
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [v for row in matrix for v in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
嵌套推导式里的
for顺序 和写嵌套for一样:外层在前,内层在后。
推导式 vs map / filter:
nums = range(10)
# 推导式:可读性最好
evens = [x for x in nums if x % 2 == 0]
# 同样效果
evens = list(filter(lambda x: x % 2 == 0, nums))
squares = [x * x for x in nums]
squares = list(map(lambda x: x * x, nums))
简单场景下,优先用推导式。只在已经有可复用的命名函数时,map 才更合身。
2.5 排序:sort vs sorted
a = [3, 1, 4, 1, 5]
b = sorted(a) # 返回新列表 [1, 1, 3, 4, 5]
a.sort() # 原地排序,返回 None
# 自定义 key
users = [{"name": "bob", "age": 30}, {"name": "ann", "age": 25}]
users.sort(key=lambda u: u["age"])
2.6 复制与排序陷阱
a = [3, 1, 2]
b = a # 同一对象
b.append(99)
# a == [3, 1, 2, 99]
# 想复制?
c = a.copy() # 或 a[:]、list(a)、copy.copy(a)
# 想排序且不动原列表?
d = sorted(a) # [1, 2, 3]
2.7 切片
nums = [0, 1, 2, 3, 4, 5]
nums[1:4] # [1, 2, 3]
nums[::2] # [0, 2, 4]
nums[::-1] # [5, 4, 3, 2, 1, 0]
切片返回 新列表(不修改原列表),用 [start:stop:step] 三段式。
2.8 预分配空间
如果预先知道大小,用 * 预分配比 append 快很多:
n = 10_000
# 推荐
buf = [None] * n
for i in range(n):
buf[i] = i * i
# 不推荐:每次 append 都要做边界检查 + 可能扩容
buf = []
for i in range(n):
buf.append(i * i)
2.9 bisect 维护有序列表
import bisect
xs = []
for x in [3, 1, 4, 1, 5, 9, 2, 6]:
bisect.insort(xs, x) # 仍保持升序
print(xs) # [1, 1, 2, 3, 4, 5, 6, 9]
如果插入极频繁,建议换
SortedList(sortedcontainers库)。
2.10 itertools 工具箱
import itertools
# 累加、chain、groupby
list(itertools.accumulate([1, 2, 3, 4])) # [1, 3, 6, 10]
list(itertools.chain([1, 2], [3, 4])) # [1, 2, 3, 4]
[k for k, _ in itertools.groupby("AAABBC")] # ['A', 'B', 'C']
2.11 列表 vs 其它容器
| 容器 | 是否有序 | 是否可变 | 重复元素 | 索引访问 | 适用 |
|---|---|---|---|---|---|
list |
✅ | ✅ | 允许 | O(1) | 默认的"数组" |
tuple |
✅ | ❌ | 允许 | O(1) | 不可变记录、dict key |
set |
❌ | ✅ | 不允许 | — | 去重 / O(1) 成员判断 |
dict |
插入序 | ✅ | key 唯一 | O(1) | 键值映射 |
deque |
✅ | ✅ | 允许 | O(n) | 头尾 O(1) 插入 / 删除 |
array.array |
✅ | ✅ | 允许 | O(1) | 大量同类型数值(省内存) |
numpy.ndarray |
✅ | ✅ | 允许 | O(1) | 向量化数值计算 |
频繁在 两端 操作,请用
collections.deque:from collections import deque dq = deque([1, 2, 3]) dq.appendleft(0) # O(1) dq.popleft() # O(1)
频繁查找 →
set/dict:needles = ["a", "b", "c"] big = ["..."] * 10_000 # ❌ O(n*m) hits = [x for x in needles if x in big] # ✅ O(n+m) big_set = set(big) hits = [x for x in needles if x in big_set]
「只读不写」考虑
tuple:不可变,可哈希、可作为 dict key、多线程更安全:key = (1, 2) # OK # key = [1, 2] # TypeError: unhashable type: 'list'
3. 字典(dict)
3.1 创建
empty = {}
person = {"name": "Alice", "age": 25, "city": "New York"}
# 动态构造
d = dict(name="Bob", age=30)
e = dict([("a", 1), ("b", 2)])
f = {x: x * x for x in range(5)} # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
3.2 读写与删除
person["email"] = "alice@example.com" # 新增 / 修改
person.setdefault("country", "US") # 已有就保留,没有就插入
age = person.get("age", 0) # 安全取,缺省 0
# 等价:person["age"] if "age" in person else 0
del person["city"] # 删除 key
email = person.pop("email", None) # 弹出 key,缺省 None
3.3 遍历
for key, value in person.items():
print(key, "->", value)
for key in person: # 默认遍历 key
print(key, person[key])
for value in person.values():
print(value)
3.4 合并
defaults = {"host": "localhost", "port": 8080}
override = {"port": 9090, "debug": True}
# Python 3.9+
merged = defaults | override
# {"host": "localhost", "port": 9090, "debug": True}
# 旧写法
merged = {**defaults, **override}
3.5 字典推导式
names = ["alice", "bob", "carol"]
lengths = {n: len(n) for n in names} # {"alice": 5, "bob": 3, "carol": 5}
# 过滤
short = {n: len(n) for n in names if len(n) <= 4}
key 必须是 可哈希 的。
list/dict/set不能作为 key,tuple/frozenset/ 字符串 / 数字 都可以(前提是内容也都可哈希)。
3.6 defaultdict / Counter
频繁地"按 key 累加"或"分组"时,这两个工具很省事:
from collections import defaultdict, Counter
# 分组
groups = defaultdict(list)
for name, team in [("alice", "A"), ("bob", "B"), ("carol", "A")]:
groups[team].append(name)
# {"A": ["alice", "carol"], "B": ["bob"]}
# 计数
Counter("abracadabra").most_common(2)
# [('a', 5), ('b', 2)]
4. 元组(tuple)
4.1 创建与不可变性
empty = ()
point = (10.0, 20.0)
one = (1,) # 注意逗号,单元素元组必须保留
不可变指的是 元组本身(不能增删改元素),但元素本身如果是可变的,仍可改:
t = (1, [2, 3], 4)
t[1].append(99)
print(t) # (1, [2, 3, 99], 4)
4.2 解包
x, y = point
print(x, y) # 10.0 20.0
# 高级用法
first, *rest = [1, 2, 3, 4]
# first = 1, rest = [2, 3, 4]
a, b, *_, c = range(10)
# a=0, b=1, c=9
# 同时解包多个
for name, age in [("alice", 30), ("bob", 25)]:
print(name, age)
4.3 namedtuple:给元组起字段名
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(10, 20)
print(p.x, p.y) # 10 20
print(p) # Point(x=10, y=20)
需要可变字段、默认值、继承等更多能力时,切到
dataclass(Python 3.7+):
from dataclasses import dataclass
@dataclass
class Point:
x: float
y: float = 0.0 # 默认值
4.4 元组可哈希
{(1, 2): "ok"} # OK
{([1, 2]): "fail"} # TypeError: unhashable type: 'list'
5. 容器之间的转换
xs = [1, 2, 2, 3, 1]
list(xs) # [1, 2, 2, 3, 1]
tuple(xs) # (1, 2, 2, 3, 1)
set(xs) # {1, 2, 3}
# 字典与列表互转
items = [("a", 1), ("b", 2)]
dict(items) # {"a": 1, "b": 2}
list({"a": 1, "b": 2}) # ["a", "b"] (默认取 key)
list({"a": 1, "b": 2}.items()) # [("a", 1), ("b", 2)]
6. 选型清单
| 场景 | 选什么 |
|---|---|
| 顺序集合,常按位置访问 | list |
需要去重 / 频繁 in 判定 |
set |
| 键值对 | dict |
| 不可变记录、返回多个值、当 dict key | tuple |
| 需要命名字段但又不想定义类 | namedtuple |
| 复杂数据结构 + 默认值 / 方法 | dataclass |
| 顺序访问 + 频繁头尾操作 | collections.deque |
7. 易错点
dict在 3.7 之前不保证插入序;3.7+ 是有保证的。跨版本代码要小心。dict在迭代过程中 不能增删元素;要么先list(d.items())备份,要么用临时 dict 收集。tuple不可变 ≠ 安全。t = (1, [2])看似安全但t[1].append(3)仍然有效。set里只能放可哈希元素,list/dict/set自身都不行。- 字典的
pop(key)缺省值一定要传;不传会抛KeyError。 lst = lst + [x]不是原地拼接! 它会创建新列表,O(n) 拷贝。要追加请用lst.append(x);要扩展请用lst.extend([...])。a == b和a is b不同:==比较内容,is比较身份(地址)。- 多列表共享引用:小心浅拷贝后修改内层对象。
解法:a = [[1]] * 3 a[0][0] = 9 a # [[9], [9], [9]] —— 三个子列表其实是同一个a = [[1] for _ in range(3)]。 - 「列表很慢」的错觉:很多"慢"其实来自
in/remove/ 头部操作,而不是 list 本身。理解复杂度能避免很多误优化。
8. 小结
- list 有序可变、索引 O(1),适合"数组"。
- dict 哈希映射,键值查找 O(1),适合"查找表 / 配置"。
- tuple 不可变序列,可哈希,可作为 dict key 或函数多返回值。
- 选错容器会付出性能 / 可维护性代价;理解复杂度比"哪个看起来最顺"更可靠。
- 默认用列表,频繁头尾用
deque,频繁查找用set/dict,只读不写考虑tuple。 - 列表推导式 ≈
map+filter,但更直观、更快。 - 真正写大数值数组时跳出 list,用
array.array或numpy。