第8章 8.1 collections 高级数据结构
🎯 开场 3 分钟:为什么要学这个?
上一章我们做了一个银行账户管理系统,用 Python 的基础列表和字典存储用户数据。你有没有发现:查一个用户要遍历整个列表、统计不同账户类型要写一堆循环、数据结构嵌套太深自己都晕了?
痛点来了:如果你管理的是 100 万用户的银行数据,用普通列表查一个人要遍历 100 万次,够慢的吧?
这一章我们要学一个新武器——collections 模块。它是 Python 内置的「高级数据结构军火库」,专门解决「数据太多不好管」的问题。学完你能:
- 用
Counter秒秒钟统计词频、统计商品销量 - 用
defaultdict不用每次判断 key 是否存在 - 用
namedtuple让数据像有名字的表格一样好读

🧱 基础 25 分钟:核心概念
8.1.1 Counter:数数神器
是什么:专门用来「计数」的数据结构,像个高级版的字典。
生活类比:想象你开了一家奶茶店,营业结束后要数一下今天卖了多少杯不同口味的饮品。Counter 就是你的「收银小助手」,自动帮你按口味分类统计。
为什么要用:比手动写字典计数简洁 10 倍,还自带一堆实用方法。
from collections import Counter
# 场景:统计一篇文章里出现最多的单词
words = ["apple", "banana", "apple", "cherry", "banana", "apple"]
counter = Counter(words)
print(counter)
# 输出:Counter({'apple': 3, 'banana': 2, 'cherry': 1})
Counter 接收任何可迭代对象,自动统计每个元素出现次数。上面代码把单词列表丢进去,马上就知道 apple 出现了 3 次。
常用方法:
# 找出出现最多的 2 个
print(counter.most_common(2))
# 输出:[('apple', 3), ('banana', 2)]
# 计数运算:合并两个 Counter
c1 = Counter(["a", "b", "a"])
c2 = Counter(["a", "c", "a", "a"])
print(c1 + c2)
# 输出:Counter({'a': 4, 'b': 1, 'c': 1})
8.1.2 defaultdict:不用判断 key 是否存在的字典
是什么:一种「自带默认值」的字典,访问不存在的 key 不会报错。
生活类比:就像一个贴心的助理,你问「小王今天汇报了吗」,即使小王还没汇报过,助理也不会愣住,而是给你一个「暂无汇报」的默认回复,而不是报错。
为什么要用:再也不用写 if key in dict 的繁琐判断了。
from collections import defaultdict
# 场景:按部门分组统计员工数量
departments = defaultdict(list) # 默认值是空列表
departments["技术部"].append("张三")
departments["技术部"].append("李四")
departments["市场部"].append("王五")
print(departments)
# 输出:defaultdict(<class 'list'>, {'技术部': ['张三', '李四'], '市场部': ['王五']})
注意这里 departments["财务部"] 从没被赋值过,但直接访问也不会报错,会返回空列表。
设置默认工厂:
# 统计每个部门的人数(用 int 作为默认值,默认是 0)
dept_count = defaultdict(int)
dept_count["技术部"] += 5
dept_count["技术部"] += 3
dept_count["市场部"] += 2
print(dept_count["财务部"]) # 不存在,返回默认值 0
# 输出:0
8.1.3 namedtuple:让数据像表格一样有名字
是什么:创建一个「像元组一样不可变、又像对象一样有属性名」的数据类型。
生活类比:普通元组像一排没有标签的抽屉,my_tuple[0]、my_tuple[1] 完全靠脑子记哪个抽屉放什么。namedtuple 像给每个抽屉贴了标签,my_point.x 一眼就知道是坐标。
为什么要用:代码可读性爆炸式提升,再也不用猜 data[0] 是什么意思。
from collections import namedtuple
# 定义一个「学生」数据类型
Student = namedtuple("Student", ["name", "age", "grade"])
# 创建实例
xiaoming = Student("小明", 15, "高一")
print(xiaoming.name)
print(xiaoming.age)
print(xiaoming.grade)
# 输出:小明
# 15
# 高一
# 像普通元组一样支持索引和解包
print(xiaoming[0]) # 输出:小明
name, age, grade = xiaoming # 解包

8.1.4 deque:双端队列,两头都能操作
是什么:一种可以在两端快速添加/删除元素的队列。
生活类比:像排队买奶茶的队伍,deque 允许你从队伍前面(优先通道)或后面(新来的人)快速加入。从前面插入叫 appendleft,从后面插入叫 append。
为什么要用:普通列表在头部插入/删除是 O(n) 慢操作,deque 是 O(1) 极速。
from collections import deque
# 场景:模拟浏览器前进/后退功能
history = deque(["page1", "page2", "page3"], maxlen=5) # 最多保存5个
history.append("page4")
history.appendleft("page0") # 前面插入
print(history)
# 输出:deque(['page0', 'page1', 'page2', 'page3', 'page4'], maxlen=5)
# 双向操作
history.pop() # 从右边弹出
history.popleft() # 从左边弹出
8.1.5 OrderedDict:记得插入顺序的字典
是什么:一个会记住 key 插入顺序的字典(Python 3.7+ 普通 dict 已经有序,这个了解即可)。
为什么要用:在 Python 3.7 之前,只有 OrderedDict 能保证字典顺序。现在更多是语义化表达「我需要有序」时使用。
from collections import OrderedDict
# 场景:保存配置项的顺序
config = OrderedDict()
config["theme"] = "dark"
config["language"] = "zh"
config["font_size"] = 14
print(list(config.keys()))
# 输出:['theme', 'language', 'font_size'](按插入顺序)
8.1.6 ChainMap:把多个字典串成一个
是什么:把多个字典「链」在一起,假装是一个大字典。
生活类比:像多本字典放书架上,ChainMap 让你查词时先翻第一本,没有再去第二本,一直找到为止。
为什么要用:合并多个配置源(比如命令行参数 + 环境变量 + 默认配置),不用真的合并。
from collections import ChainMap
# 场景:多层配置优先级
default_config = {"theme": "light", "debug": False}
user_config = {"theme": "dark"} # 用户配置覆盖默认配置
cli_config = {"debug": True} # 命令行最高优先级
# 按优先级从低到高排列
combined = ChainMap(cli_config, user_config, default_config)
print(combined["theme"]) # dark(用户覆盖了默认)
print(combined["debug"]) # True(命令行最高)
print(combined["font"]) # KeyError(所有来源都没有)
🔥 实战 35 分钟:3 个递进的小项目
项目 1:单词计数器(5 分钟)
场景:统计一段文字里出现最多的 5 个单词。
from collections import Counter
import re
text = """
Python is a powerful programming language Python is easy to learn
Python is versatile Python is used in data science machine learning
web development automation and more Python has a large standard library
"""
# 简单分词:转小写、提取英文单词
words = re.findall(r'[a-z]+', text.lower())
# 统计
counter = Counter(words)
# 输出最常见的 5 个
for word, count in counter.most_common(5):
print(f"{word}: {count}次")
预期输出:
python: 9次
is: 4次
a: 3次
and: 2次
in: 1次
一句话解释:most_common() 方法按出现次数降序排列,一行搞定排名统计。
项目 2:班级成绩管理系统(15 分钟)
场景:从 CSV 文件读取学生成绩,按科目统计平均分、最高分,找出需要帮助的学生。
from collections import defaultdict, Counter
# 模拟 CSV 数据(实际项目从文件读入)
csv_data = """姓名,科目,成绩
张三,数学,85
李四,数学,45
王五,数学,92
张三,语文,78
李四,语文,60
王五,语文,88
张三,英语,90
李四,英语,35
王五,英语,95
"""
# 解析 CSV
lines = csv_data.strip().split('\n')
header = lines[0].split(',')
students = defaultdict(dict)
for line in lines[1:]:
name, subject, score = line.split(',')
students[name][subject] = int(score)
# 统计每个科目的平均分和最高分
subject_stats = defaultdict(list)
for name, scores in students.items():
for subject, score in scores.items():
subject_stats[subject].append(score)
print("=" * 40)
print("各科目成绩统计")
print("=" * 40)
for subject, scores in subject_stats.items():
avg = sum(scores) / len(scores)
max_score = max(scores)
print(f"{subject}:平均分 {avg:.1f},最高分 {max_score}")
# 找出需要帮助的学生(单科低于 60 分)
print("\n" + "=" * 40)
print("需要重点关注的学生")
print("=" * 40)
for name, scores in students.items():
weak_subjects = [sub for sub, score in scores.items() if score < 60]
if weak_subjects:
print(f"{name}:{', '.join(weak_subjects)} 需要加强")
预期输出:
========================================
各科目成绩统计
========================================
数学:平均分 74.0,最高分 92
语文:平均分 75.3,最高分 88
英语:平均分 73.3,最高分 95
========================================
需要重点关注的学生
========================================
李四:数学 需要加强
李四:英语 需要加强
一句话解释:defaultdict(dict) 让嵌套数据操作变得零负担,不用先判断 key 是否存在。
项目 3:命令行待办清单工具(15 分钟)
场景:做一个带优先级的待办清单,支持添加、完成、查看统计。
from collections import deque, Counter
class TodoList:
def __init__(self):
# 前面是紧急的,后面是不紧急的
self.items = deque()
self.completed = []
def add(self, task, priority=0):
"""添加任务,priority 越大越紧急"""
# 扫描是否已存在
for i, (t, p) in enumerate(self.items):
if t == task:
print(f"'{task}' 已在清单中")
return
# 插入到正确位置
inserted = False
for i in range(len(self.items)):
if self.items[i][1] < priority:
self.items.insert(i, (task, priority))
inserted = True
break
if not inserted:
self.items.append((task, priority))
print(f"添加成功:{task}(优先级: {priority})")
def complete(self):
"""完成第一个任务"""
if not self.items:
print("清单是空的!")
return
task, priority = self.items.popleft()
self.completed.append((task, priority))
print(f"已完成:{task}")
def show(self):
"""显示当前清单"""
if not self.items:
print("清单是空的!")
return
print("\n📋 待办清单")
print("-" * 30)
for i, (task, priority) in enumerate(self.items):
tag = "🔥" if priority >= 5 else " "
print(f"{tag}{i+1}. {task}")
def stats(self):
"""显示统计"""
print(f"\n📊 统计")
print(f"待完成:{len(self.items)} 项")
print(f"已完成:{len(self.completed)} 项")
# 统计已完成任务的优先级分布
if self.completed:
priorities = [p for _, p in self.completed]
counter = Counter(priorities)
print("完成优先级分布:", dict(counter))
# 使用演示
if __name__ == "__main__":
todo = TodoList()
todo.add("写周报", priority=3)
todo.add("回复邮件", priority=5)
todo.add("开会", priority=8)
todo.add("写代码", priority=6)
todo.add("写周报") # 测试重复
todo.show()
todo.complete()
todo.complete()
todo.stats()
todo.show()
预期输出:
添加成功:写周报(优先级: 3)
添加成功:回复邮件(优先级: 5)
添加成功:开会(优先级: 8)
添加成功:写代码(优先级: 6)
'写周报' 已在清单中
📋 待办清单
------------------------------
🔥1. 开会
🔥2. 写代码
3. 回复邮件
4. 写周报
已完成:开会
已完成:写代码
📊 统计
待完成:2 项
已完成:2 项
完成优先级分布:{8: 1, 6: 1}
📋 待办清单
------------------------------
1. 回复邮件
2. 写周报
一句话解释:deque 的 popleft() 让完成任务的取出操作是 O(1) 极速,比列表的 pop(0) 快得多。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:Counter 加法 vs 减法
# ❌ 错误示例:想删除某些元素,用了加法
c1 = Counter(["a", "b", "c"])
c2 = Counter(["b"])
c3 = c1 + c2 # 这样是合并,不是删除
print(c3) # Counter({'a': 1, 'b': 2, 'c': 1})
# ✅ 正确示例:用减法删除计数
c1 = Counter(["a", "b", "b", "c"])
c2 = Counter(["b"])
c3 = c1 - c2 # 相减,计数为0或负数的会被删除
print(c3) # Counter({'a': 1, 'b': 1, 'c': 1})
坑 2:defaultdict 访问不存在的 key
# ❌ 错误示例:期望返回 None 或报错
from collections import defaultdict
d = defaultdict(int)
print(d["不存在"]) # 返回 0,不是 None
# ✅ 正确示例:如果确实需要 None,用 list 作为默认值
d = defaultdict(list)
print(d["不存在"]) # 返回空列表 []
坑 3:namedtuple 的不可变性
# ❌ 错误示例:尝试修改 namedtuple
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(1, 2)
p.x = 10 # 报错:AttributeError: can't set attribute
# ✅ 正确示例:用 _replace 创建修改后的副本
p = Point(1, 2)
p2 = p._replace(x=10) # 返回新的 Point(10, 2)
print(p2.x) # 10
print(p.x) # 1,原来的不变
坑 4:deque 的 maxlen 满了之后的行为
# ❌ 错误示例:以为满了会报错
d = deque([1, 2, 3], maxlen=3)
d.append(4) # 不会报错,最左边的 1 被挤掉了
print(list(d)) # [2, 3, 4]
# ✅ 正确示例:满了自动挤出旧数据,不需要手动管理
d = deque(maxlen=5)
for i in range(10):
d.append(i)
print(list(d)) # [5, 6, 7, 8, 9],只保留最后5个
坑 5:ChainMap 不修改原始字典
# ❌ 错误示例:以为赋值会修改原始字典
from collections import ChainMap
d1 = {"a": 1}
d2 = {"b": 2}
cm = ChainMap(d1, d2)
cm["a"] = 100 # 实际上修改的是 d1
print(d1) # {'a': 100}
# ✅ 正确示例:想整体修改,要直接操作原始字典
# ChainMap 只读,不写!如果需要写操作,手动合并
性能小贴士:deque vs list
# 性能对比:头部插入 10000 次
import time
from collections import deque
# 列表头部插入(慢)
start = time.time()
lst = []
for i in range(10000):
lst.insert(0, i)
print(f"list insert(0): {time.time() - start:.3f}s")
# deque 头部插入(快)
start = time.time()
dq = deque()
for i in range(10000):
dq.appendleft(i)
print(f"deque appendleft: {time.time() - start:.3f}s")
# deque 比 list 快 100 倍以上
调试技巧:快速查看数据结构
# 用 vars() 快速查看对象的所有属性
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
p = Point(1, 2)
print(vars(p)) # {'x': 1, 'y': 2}
# 用 __len__ 和 __contains__ 检查内容
from collections import Counter
c = Counter(["a", "b", "a"])
print(len(c)) # 2(有2种不同元素)
print("a" in c) # True
print(c["a"]) # 2
✏️ 练习题
练习 1(2 分钟):Counter 基础操作
- 输入:["红", "蓝", "红", "绿", "蓝", "红"]
- 预期输出:红: 3,蓝: 2,绿: 1
- 提示:用 most_common() 方法
练习 2(3 分钟):添加判断逻辑
- 输入:在 Counter 结果中,只打印出现次数大于 2 的元素
- 预期输出:只显示 "红: 3"
- 提示:遍历 most_common() 结果,加个 if count > 2 判断
练习 3(5 分钟):处理新数据
- 输入:统计句子 "python python java python ruby java python" 中单词频率
- 预期输出:{'python': 4, 'java': 2, 'ruby': 1}
- 提示:直接用空格 split 后传给 Counter
练习 4(8 分钟):组合两种数据结构
- 输入:用 defaultdict 统计多个学生各科成绩的平均分
- 预期输出:每个学生显示各科平均分
- 提示:defaultdict(list) 存所有成绩,最后算平均
练习 5(5 分钟):分析报错并修复
- 输入:运行以下代码,找出错误
from collections import namedtuple
Student = namedtuple("Student", ["name", "age"])
s = Student("小明", 15)
s.age = 16
- 预期输出:修复后正确输出
Student(name='小明', age=16) - 提示:namedtuple 不可变,要用
_replace方法
作业:做一个「班级信息管理系统」
-
需求描述:管理一个班级的学生信息,每个学生有姓名、年龄、班级;支持按班级查看学生列表、统计各班人数、找出年龄最大的学生。
-
功能点:
1. 添加学生(姓名、年龄、班级)
2. 按班级查询学生列表
3. 统计各班人数
4. 找出年龄最大/最小的学生 -
加分项:
1. 用 namedtuple 存储学生信息
2. 用 Counter 统计各班人数分布 -
验收标准:能跑起来、输出符合预期、代码有注释
📚 总结 + 资源
本文学了 3 个核心点:
1. Counter 是计数神器,一行代码搞定词频统计
2. defaultdict 让你告别 if key in dict 的繁琐判断
3. namedtuple 让数据可读性爆炸提升,代码再也不乱
延伸学习资源:
- Python 官方文档 - collections 模块(最权威的参考)
- 《Python 编程:从入门到实践》第 9 章「类」(深入面向对象)
- 视频:B 站小甲鱼《零基础入门学习 Python》第 45 讲
互动钩子:你在 XX 场景(数据分析、爬虫、游戏开发...)用过 collections 模块吗?评论区聊聊你的使用经验,老粉优先回复!
下一章我们要学习 itertools 迭代器工具,教你用几行代码搞定排列组合、循环迭代的骚操作,让你的代码「跑得更快、写得更少」!

评论(0)