R / Richie全部文章 ↑

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。