第 4 章 · 列表、字典、集合

Python 的容器四件套(列表、元组、字典、集合)是它「脚本语言威力」的主力:一个字典顶 Java 课的 HashMap + getter/setter 全家桶,一个列表切片顶 C 的指针算术。本章讲四件套的性格差异、选型表、以及高频惯用法。数据结构课的线性表、哈希表(第 2、7 章)在这里都有工程实体,对照着学收益翻倍。

4.1 列表(list):万能的动态数组

lst = [3, 1, 4, 1, 5]
lst.append(9)          # 尾加,O(1) 摊还(数据结构课第 1 章的动态数组)
lst.insert(0, 7)       # 头插,O(n)(全体后移)
lst.pop()              # 尾删并返回
lst.remove(4)          # 按值删第一个匹配
lst.sort()             # 原地排序(Timsort,数据结构课第 8 章)
lst.reverse()
lst[1:3]               # 切片:新列表 [1, 4]
[0] * 5                # [0,0,0,0,0](快速初始化)
len(lst), 5 in lst     # 长度、成员判断
sorted(lst)            # 返回新列表(不动原列表)

列表是「动态数组」实现(数据结构课第 12 章:连续内存 + 按需扩容),所以尾部操作 O(1)、随机访问 O(1)、头部/中部插入 O(n)(要搬)。高频头插用 collections.deque(双端队列,第 7 章)。

引用语义(第 2 章名字贴的重头戏):b = a 是同一对象,b.append 时 a 也变;真复制用 a.copy()(浅)或 copy.deepcopy(a)(深,嵌套结构必须)。Java 课第 5 章数组的同款坑,Python 里因为默认全是引用,杀伤面更大。

4.2 字典(dict):哈希表的官方实现

user = {"name": "思源", "age": 21}
user["school"] = "西亚斯"       # 增/改
user["name"]                    # 查:不存在则 KeyError!
user.get("phone", "未填")       # 查:带默认值,不炸
"user" in user                  # 键是否存在
del user["age"]                 # 删
for k, v in user.items():       # 遍历键值对
    print(k, v)

字典是哈希表(数据结构课第 7 章:桶 + 冲突处理),查询 O(1)。三个必会惯用法:

# ① get 带默认:统计词频
for w in words:
    count[w] = count.get(w, 0) + 1
# ② setdefault / defaultdict(第 7 章):值是列表的字典
from collections import defaultdict
groups = defaultdict(list)
groups[key].append(item)        # 键不存在自动建空列表
# ③ 字典推导式(第 3 章)
squares = {n: n*n for n in range(5)}

字典键的要求:可哈希(不可变:字符串、数字、元组可以;列表不行——数据结构课第 7 章「哈希键稳定性」)。值随便。Python 3.7+ 字典保持插入顺序(哈希表的意外赠品,写有序配置很方便)。

4.3 元组(tuple)与集合(set)

元组 = 不可变列表:point = (3, 4),不能增删改;用途:固定结构的记录(坐标、键值对)、字典的键、函数多返回值(第 3 章)。a, b = b, a 交换变量的魔法,本质是元组打包解包。「可变 vs 不可变」的选型:会变的用列表,当常量记录用的用元组(还能当字典键)。

集合 = 无序不重复,哈希实现:

tags = {"python", "web"}
tags.add("sql")                  # 增
"python" in tags                 # O(1) 成员判断(列表是 O(n)!)
a & b, a | b, a - b              # 交、并、差(离散数学课第 1 章的运算)
set([1,1,2,2])                   # {1,2} 去重神器

去重(set(lst))和成员判断(O(1))是集合两大日常;数学运算直接对标离散数学(交集并集差集,本站离散课第 1 章)。

4.4 选型表与惯用法合集

需求用例
有序可变序列list待办清单
固定记录tuple坐标、RGB
键 → 值查询dict用户档案、计数器
去重/成员判断set已见过的 ID
高频头插deque滑动窗口

惯用法合集(背下来,代码量减半):

if not lst: ...                    # 空列表判空(空序列为 False,Pythonic)
a, b = lst[0], lst[-1]             # 首尾
top3 = sorted(scores, reverse=True)[:3]   # 前三名
for i, v in enumerate(lst): ...    # 带下标遍历
d = dict(zip(keys, values))        # 平行列表变字典

常见错误

  • 引用共享(4.1 头号坑)。grid = [[0]*3]*3 的三行是同一个列表(乘法复制引用!),改 grid[0][0] 三行全变;正确姿势 [[0]*3 for _ in range(3)]。
  • 字典遍历时增删键。RuntimeError: dictionary changed size;遍历 list(d.keys()) 副本或收集后处理。
  • 列表当字典键。TypeError: unhashable;换元组(坐标当键:(x, y): value)。
  • remove 前不判断。不存在则 ValueError;先 if x in lst 或用异常处理(第 5 章)。

练习

  1. 动手:词频统计器(第 3 章练习的容器版):一段英文,用字典数词频,打印 Top3(sorted + 切片);再对比 Java 课第 10 章 HashMap 版的代码量。
  2. 动手:复现 4.4 的 grid 坑(打印 id 验证),用推导式修复;写一个 5×5 的对角线为 1 的矩阵(推导式嵌套)。
  3. 实现:通讯录程序:字典存储(姓名 → (电话, 邮箱) 元组),支持增、删、改、模糊查询(名字包含子串),循环菜单(第 3 章 while + match)。
  4. 思考:Python 3.7 字典「保持插入顺序」改变了什么?对比 Java 课 HashMap(无序)与 LinkedHashMap(有序要换类),讨论「语言内置常见需求」与「标准库分层提供」两种设计哲学的取舍。