🎯 学习目标
- 掌握列表的增删改查、切片、排序与深浅拷贝
- 理解元组不可变的意义,熟练使用打包/解包、命名元组
- 掌握字典的键值操作、
get/setdefault、遍历方式与推导式 - 掌握集合的去重与交并差集运算
- 能根据场景选择合适的数据结构,了解各自时间复杂度
- 完成词频统计案例(NLP 最基础的统计任务雏形)
1四大容器速览
| 类型 | 语法 | 有序? | 可重复? | 可变? | 检索方式 |
|---|---|---|---|---|---|
| 列表 list | [1, 2, 3] | ✅ 有序 | ✅ | ✅ 可变 | 下标 |
| 元组 tuple | (1, 2, 3) | ✅ 有序 | ✅ | ❌ 不可变 | 下标 |
| 字典 dict | {"a": 1} | ✅(3.7+保序) | 键唯一 | ✅ 可变 | 键 key |
| 集合 set | {1, 2, 3} | ❌ 无序 | ❌ 唯一 | ✅ 可变 | 哈希 |
2列表 list:最常用的有序容器
2.1 创建
nums = [1, 2, 3]
mixed = [1, "hello", True, [2, 3]] # 元素类型可以不同
empty = []
chars = list("python") # ['p','y','t','h','o','n']
r = list(range(1, 6)) # [1,2,3,4,5]
rep = [0] * 5 # [0,0,0,0,0]
matrix = [[1, 2], [3, 4]] # 二维列表(矩阵)
print(matrix[1][0]) # 3 第二行第一列
2.2 查询:索引、切片、遍历
fruits = ["苹果", "香蕉", "橙子", "葡萄"]
fruits[0] # '苹果'
fruits[-1] # '葡萄'
fruits[1:3] # ['香蕉', '橙子'](切片语法与字符串完全相同)
len(fruits) # 4
"香蕉" in fruits # True
fruits.index("橙子") # 2 首次出现的下标(不存在会报错)
fruits.count("苹果") # 1 统计次数
for f in fruits: # 直接遍历元素(推荐)
print(f)
for i, f in enumerate(fruits): # 同时要序号
print(i, f)
2.3 增:append / extend / insert
a = [1, 2]
a.append(3) # [1,2,3] 尾部追加【单个元素】,最常用
a.append([4, 5]) # [1,2,3,[4,5]] 注意!整个列表作为一个元素
a.extend([6, 7]) # [1,2,3,[4,5],6,7] 展开后逐个加入
a += [8] # 等价 extend
a.insert(0, 0) # [0,1,2,...] 在指定下标插入(效率低,慎用)
append 与 extend 的区别是初学者高频面试点:append 把参数整个塞进去,extend 把可迭代对象拆开追加。
2.4 删:pop / remove / clear / del
a = ["a", "b", "c", "d"]
a.pop() # 弹出并返回 'd'(默认末尾,类似栈)
a.pop(0) # 弹出下标0的元素 'a'(弹出开头效率低)
a.remove("b") # 按值删除第一个匹配(不存在会 ValueError)
a.clear() # 清空 → []
del a # 删除整个变量;也可 del a[1:3] 切片删除
2.5 改
a = [10, 20, 30, 40]
a[0] = 99 # [99,20,30,40] 按下标改
a[1:3] = [200, 300] # 切片批量替换
a[1:3] = [] # 等价删除切片
a.sort() # 原地排序(直接改变列表)
a.reverse() # 原地逆序
2.6 排序:最重要的列表操作之一
nums = [3, 1, 4, 1, 5, 9, 2]
nums.sort() # 原地升序 [1,1,2,3,4,5,9]
nums.sort(reverse=True) # 原地降序
# sort() 没有返回值(返回 None),常见错误:
# b = a.sort() → b 是 None!
# 需要新列表用内置函数 sorted()
ordered = sorted([3, 1, 2]) # [1,2,3],原列表不变
rev = sorted([3, 1, 2], reverse=True)
# 按规则排序:key 函数(极其常用)
words = ["banana", "fig", "apple"]
sorted(words, key=len) # 按长度:['fig','apple','banana']
sorted(words, key=str.lower) # 忽略大小写
students = [("张三", 88), ("李四", 76), ("王五", 95)]
sorted(students, key=lambda x: x[1]) # 按分数升序
sorted(students, key=lambda x: x[1], reverse=True) # 分数降序
# 多关键字:先按年级,再按分数
sorted(students, key=lambda x: (x[0], -x[1]))
# 中文按拼音排序
import locale
locale.setlocale(locale.LC_COLLATE, "Chinese_China.936")
sorted(["张三", "李四", "王五"], key=locale.strxfrm)
2.7 列表的深浅拷贝(经典坑)
a = [1, 2, 3]
b = a # 不是拷贝!只是多一个标签指向同一个列表
b[0] = 99
print(a) # [99,2,3] —— a 也被改了!
# 浅拷贝:复制"外层壳子",但内部元素仍是共享引用
a = [[1, 2], [3, 4]]
b = a.copy() # 或 a[:]、list(a)
b[0] = [9, 9] # 改外层:不影响 a
print(a) # [[1,2],[3,4]]
b = a.copy()
b[0][0] = 99 # 改【内层】元素:a 也跟着变!
print(a) # [[99,2],[3,4]]
# 深拷贝:内外层全部独立,互不影响
import copy
b = copy.deepcopy(a)
b[0][0] = 0
print(a) # 不受影响
口诀:一层用浅拷(copy() / 切片 [:]),嵌套用深拷(copy.deepcopy())。NLP 中处理嵌套语料结构时尤其要注意。
3元组 tuple:不可变的列表
t = (1, 2, 3)
single = (42,) # 单元素元组必须有逗号!(42) 只是括号表达式
t2 = 1, 2, 3 # 不加括号也是元组(打包)
t3 = tuple([1, 2])
t[0] # 1 可索引、切片、in、len,和列表一样
# t[0] = 99 # TypeError!创建后不能增删改
# t.append(4) # AttributeError!没有 append/sort 等修改方法
# 元组运算
(1, 2) + (3,) # (1,2,3)
("a",) * 3 # ('a','a','a')
打包与解包(元组的杀手锏)
# 打包
student = ("张三", 20, "计算机")
# 解包:一行同时赋值多个变量
name, age, major = student
print(name, age, major)
# 函数多返回值本质就是元组
def min_max(data):
return min(data), max(data) # 打包返回
lo, hi = min_max([3, 1, 4, 1, 5]) # 解包接收
print(lo, hi) # 1 5
# 交换变量
a, b = b, a
# 解包进阶:* 收集
first, *middle, last = (1, 2, 3, 4, 5)
print(first, middle, last) # 1 [2,3,4] 5
t = (1, 2, 3)
single = (42,) # 单元素元组必须有逗号!(42) 只是括号表达式
t2 = 1, 2, 3 # 不加括号也是元组(打包)
t3 = tuple([1, 2])
t[0] # 1 可索引、切片、in、len,和列表一样
# t[0] = 99 # TypeError!创建后不能增删改
# t.append(4) # AttributeError!没有 append/sort 等修改方法
# 元组运算
(1, 2) + (3,) # (1,2,3)
("a",) * 3 # ('a','a','a')# 打包
student = ("张三", 20, "计算机")
# 解包:一行同时赋值多个变量
name, age, major = student
print(name, age, major)
# 函数多返回值本质就是元组
def min_max(data):
return min(data), max(data) # 打包返回
lo, hi = min_max([3, 1, 4, 1, 5]) # 解包接收
print(lo, hi) # 1 5
# 交换变量
a, b = b, a
# 解包进阶:* 收集
first, *middle, last = (1, 2, 3, 4, 5)
print(first, middle, last) # 1 [2,3,4] 5为什么需要元组? ① 数据不该被修改时用元组,自带"写保护",语义更安全;② 元组可作字典的键、集合的元素(列表不行);③ 同样内容元组比列表省内存、创建更快。
# 元组可哈希,能当字典的 key
locations = {("北京", 1): "晴", ("上海", 1): "雨"}
# 命名元组:既像元组又能按名字访问,比类轻量
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)
print(p.x, p.y, p[0]) # 3 4 3
4字典 dict:按键查找
字典以键值对(key → value)存储,查找速度极快且与数据量无关(O(1) 平均)。NLP 中词表、词频、配置都用它。
4.1 创建
student = {"name": "张三", "age": 20, "major": "计算机"}
empty = {}
d = dict(name="李四", age=21) # 关键字参数构造
d2 = dict([("a", 1), ("b", 2)]) # 双元素序列构造
d3 = {}.fromkeys(["x", "y"], 0) # {'x':0,'y':0} 统一初值
# 键必须是【不可变且可哈希】类型:str/int/tuple 可以,list/dict 不行
# {[1,2]: "x"} # TypeError: unhashable type: 'list'
{(1, 2): "坐标"} # ✅ 元组可以
# 值可以是任意类型,包括另一个字典/列表
profile = {"scores": [90, 80], "contact": {"phone": "138..."}}
4.2 增删改查
d = {"name": "张三", "age": 20}
d["name"] # '张三' 取值
# d["city"] # KeyError!键不存在直接报错
d["age"] = 21 # 键已存在 → 修改
d["city"] = "北京" # 键不存在 → 新增
d.setdefault("city", "上海") # 键存在则返回原值,不存在才写入
d.setdefault("hobby", []).append("阅读") # 经典:安全初始化列表
d.get("name") # '张三'
d.get("salary") # None 键不存在不报错,返回 None
d.get("salary", 0) # 0 可指定默认值 —— 统计词频必备
d.pop("age") # 删除并返回值
d.pop("x", None) # 键不存在时返回默认值而不报错
del d["city"] # 直接删(不存在报错)
d.clear() # 清空
4.3 遍历
d = {"a": 1, "b": 2, "c": 3}
for k in d: # 默认遍历键
print(k)
for k in d.keys(): # 同上,显式
...
for v in d.values(): # 遍历值
print(v)
for k, v in d.items(): # 同时遍历键值(最常用)
print(k, v)
# 判断键是否存在(用 in,不要用 in + keys() 啰嗦写法)
if "a" in d:
print("有 a 键")
4.4 合并与更新
d1 = {"a": 1, "b": 2}
d2 = {"b": 20, "c": 3}
d1.update(d2) # d1 变成 {'a':1,'b':20,'c':3}(b 被覆盖)
merged = {**d1, **d2} # Python 3.5+:解包合并成新字典
merged = d1 | d2 # Python 3.9+:| 运算符合并
4.5 字典推导式
# 键值反转
d = {"a": 1, "b": 2}
inv = {v: k for k, v in d.items()} # {1:'a',2:'b'}
# 单词长度表
words = ["apple", "fig", "hi"]
lengths = {w: len(w) for w in words} # {'apple':5,'fig':3,'hi':2}
5集合 set:去重与关系运算
s = {1, 2, 3}
empty = set() # 空集合只能用 set()!{} 是空字典
set("aabbcc") # {'a','b','c'} 从可迭代对象创建
set([1, 1, 2, 3, 3]) # {1,2,3} 自动去重
# 增删
s.add(4)
s.add(3) # 已存在无效果
s.remove(1) # 删除(不存在报 KeyError)
s.discard(99) # 删除(不存在也不报错,更安全)
s.pop() # 随机弹出一个
len(s); 3 in s # 长度、成员判断(集合的 in 是 O(1),极快)
集合运算(数学集合那一套)
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
a & b # {3,4} 交集:共同好友
a | b # {1,2,3,4,5,6} 并集:所有标签
a - b # {1,2} 差集:我关注但对方没关注
a ^ b # {1,2,5,6} 对称差:只在一边出现
# 方法形式(可更新原集合)
a.intersection(b); a.union(b); a.difference(b)
a.isdisjoint(b) # 是否完全无交集
{1, 2} <= {1,2,3} # True,子集判断
{1,2,3} >= {1,2} # True,超集判断
性能提示:在大数据量上判断"元素是否出现过 / 是否在白名单中",用 set 的 in 是 O(1),而 list 是 O(n),差距可达上万倍。NLP 中加载停用词表后务必转成 set。
# 实战:列表去重
data = [2, 1, 2, 3, 1, 4]
list(set(data)) # [1,2,3,4] 但顺序丢失
list(dict.fromkeys(data)) # [2,1,3,4] 保序去重(Python 3.7+)
6四种容器选型与性能对比
| 需求场景 | 选择 |
|---|---|
| 有顺序的一批数据,需要按下标访问、排序 | list |
| 一批固定、不允许修改的数据;多返回值;当字典键 | tuple |
| 通过"名字/编号"快速查找对应信息(映射) | dict |
| 去重、白名单、共同/差异分析 | set |
| 操作 | list | tuple | dict | set |
|---|---|---|---|---|
| 按值/键查找 x in | O(n) | O(n) | O(1) | O(1) |
| 按下标访问 | O(1) | O(1) | — | — |
| 尾部追加 | O(1) 均摊 | 不支持 | O(1) | O(1) |
| 中间插入/删除 | O(n) | 不支持 | O(1) | O(1) |
7案例:词频统计(NLP 雏形)
给定分词后的词列表,统计每个词出现次数并输出 Top 3——这是文本分析最基础的操作:
words = ["python", "数据", "python", "机器学习", "数据", "python", "深度学习"]
# 方法1:纯字典
freq = {}
for w in words:
freq[w] = freq.get(w, 0) + 1 # 不存在时取0再加1
print(freq)
# 方法2:setdefault 风格
freq2 = {}
for w in words:
freq2.setdefault(w, 0)
freq2[w] += 1
# 方法3:collections.Counter(实际项目首选,NLP 篇会大量使用)
from collections import Counter
c = Counter(words)
print(c.most_common(3)) # [('python', 3), ('数据', 2), ('机器学习', 1)]
print(c["python"]) # 3
c.update(["python", "AI"]) # 继续累加
# 按频次排序输出整个字典
for word, count in sorted(freq.items(), key=lambda x: x[1], reverse=True):
print(word, count)
[('python', 3), ('数据', 2), ('机器学习', 1)]
8本章小结与练习
📌 关键结论
- append 加一个、extend 拆开来加;
sort()原地排返回 None,要新列表用sorted()。 - 嵌套结构拷贝:一层浅拷贝会共享内层,完全隔离用
copy.deepcopy。 - 字典取值用
get(key, 默认值)防空 KeyError;遍历用items()。 - 集合的成员判断是 O(1),停用词、白名单必须用 set。
- 统计频次直接用
collections.Counter。
练习1:a = [1,2]; b = a; b.append(3) 后 a 是什么?如何让 a 不受影响?
a 也是 [1,2,3],因为 b=a 只是同一列表的两个标签。要独立副本用 b = a.copy()(或 a[:]);若 a 内含嵌套列表则用 copy.deepcopy(a)。
练习2:两个列表 a=[1,2,3,4]、b=[3,4,5,6],求共同元素、各自独有元素、合并去重结果。
转集合:s=set(a), t=set(b);共同 s & t → {3,4};各自独有 s ^ t → {1,2,5,6}(a 独有 s-t、b 独有 t-s);合并去重 s | t → {1,2,3,4,5,6}。
练习3:不使用 Counter,用字典统计句子 "to be or not to be" 中各单词频次。
freq={}
for w in "to be or not to be".split():
freq[w] = freq.get(w, 0) + 1 得到 {'to':2,'be':2,'or':1,'not':1}。核心套路是"取值给默认 0,然后 +1 写回"。