Python 语法篇 · 第6章

内置数据结构:列表、元组、字典、集合

四大容器是 Python 处理"一批数据"的核心武器,也是 NLP 中存放语料、词表、词频的基础。本章逐一讲透并对比选型。

🎯 学习目标

  • 掌握列表的增删改查、切片、排序与深浅拷贝
  • 理解元组不可变的意义,熟练使用打包/解包、命名元组
  • 掌握字典的键值操作、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

为什么需要元组? ① 数据不该被修改时用元组,自带"写保护",语义更安全;② 元组可作字典的键、集合的元素(列表不行);③ 同样内容元组比列表省内存、创建更快。

# 元组可哈希,能当字典的 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
操作listtupledictset
按值/键查找 x inO(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 写回"。