第14章-集合与组合结构


第14章 集合与组合结构

问你一个问题:手头有一串名单,里面有人重复报了两次名,怎么把重复的去掉?用昨天学的列表,得写循环一个个比对,麻烦。Python 里有个东西天生干这个——集合,一个"自动去重"的麻袋:你把一袋子数字倒进去,重复的自动消失,[1, 2, 2, 3, 3, 3] 倒进去就剩 {1, 2, 3}

处理"有没有重复""有哪些公共元素"这类问题,集合最省事。今天先学它,再学本周真正的重头戏——列表套字典:一个列表里每个元素都是一个字典。它就像一张 Excel 表格:列表 = 整张表,每个字典 = 一行记录,字典的键 = 列名。为什么必须学它?因为从下周开始的所有项目——通讯录、记账本、网站后台——数据全都长这样。今天把它练熟,等于给后面的项目把地基打好。

集合:自动去重的"麻袋"

集合用花括号 {} 包起来,但里面不是键值对,是普通元素。核心特点就一个:自动去重——同一个元素只会出现一次。

和列表比一比,区别一目了然:

对比 列表 集合
写法 [1, 2, 3] 方括号 {1, 2, 3} 花括号
重复元素 允许 不允许,自动去掉
有顺序吗 有(按下标取) 没有(不能按下标取)
nums = {1, 2, 2, 3, 3, 3}
print(nums)          # {1, 2, 3}(重复的被自动去掉)

集合长什么样,能干什么、不能干什么:

a = {1, 2, 3}
b = {"苹果", "香蕉", "橙子"}
print(a)                  # {1, 2, 3}
print(b)                  # {'苹果', '香蕉', '橙子'}(顺序可能不同,集合无序)

# 集合不能按下标取
# print(a[0])             # 报错!TypeError: 'set' object is not subscriptable

# len 和 in 可以用
print(len(a))             # 3
print(2 in a)             # True

输出:

{1, 2, 3}
{'苹果', '香蕉', '橙子'}
3
True

三个新手坑提前说:集合没有顺序,打印顺序可能和你写的不一样,别指望 {1,2,3} 里第 0 个是 1;空集合是 set(),不是 {}——{} 是空字典,这个错几乎人人踩过;集合里的元素不能是列表(列表可变,没法"去重"),放列表会报 TypeError: unhashable type: 'list'

集合的基本操作:增删与判断

集合的增删方法和列表不一样,名字不同、行为也不同:

操作 集合方法 说明
加一个 add(值) 已存在的元素加了也没用(本来就一个)
删一个 remove(值) 删不存在的会报错
数个数 len(集合) 元素个数
判断在不在 值 in 集合 True / False
fruits = {"苹果", "香蕉"}

fruits.add("橙子")          # 加一个
print(fruits)              # {'苹果', '香蕉', '橙子'}

fruits.add("苹果")          # 加重复的 → 没有变化
print(fruits)              # {'苹果', '香蕉', '橙子'}

fruits.remove("香蕉")       # 删一个
print(fruits)              # {'苹果', '橙子'}

add 重复元素没效果,这正是"去重"的本体:

s = set()                  # 空集合(记住:不是 {})
s.add("苹果")
s.add("苹果")              # 再加一次,没反应
s.add("香蕉")
print(s)                   # {'苹果', '香蕉'}
print(len(s))              # 2(不是 3,重复的被吞了)

输出:

{'苹果', '香蕉'}
2

三个容易错的:加元素用 add不是 append——集合没有 append,用了报 AttributeError;删元素用 remove,删不存在的会报 KeyError,稳妥做法是先 in 判断;集合没有"按下标改"的说法,只有"加/减一个元素"。

交集与并集:两个集合做运算(了解即可)

集合之间能像数学课一样做运算:交集是"两边都有的",并集是"合在一起、去掉重复的"。本周能看懂、会用就行,不要求背得滚瓜烂熟:

符号 意思 例子 a={1,2,3} b={2,3,4} 结果
a & b 交集(两边都有) a & b {2, 3}
a \| b 并集(合起来去重) a \| b {1, 2, 3, 4}
a - b 差集(a 有 b 没有) a - b {1}
a = {1, 2, 3}
b = {2, 3, 4}

print(a & b)      # {2, 3}(交集)
print(a | b)      # {1, 2, 3, 4}(并集)
print(a - b)      # {1}(差集,了解即可)

输出:

{2, 3}
{1, 2, 3, 4}
{1}

实际用起来,最典型的是找"两个人共同喜欢的水果":

xiaoming = {"苹果", "香蕉", "橙子"}
xiaohong = {"香蕉", "西瓜", "葡萄"}

print(xiaoming & xiaohong)     # {'香蕉'}(两个人都喜欢的水果)

输出:

{'香蕉'}

有一点要拎出来说:&| 只对集合有效,对列表直接报错(列表不支持);想对列表做交集,先转成集合。别把 |(并集)和 or(逻辑或)搞混——{1} | {2} 是集合运算,结果是集合。交集并集今天"会看会读"就行,重点还是下面的列表套字典。

去重实战:把列表里的重复数字找出来

集合最实用的场景:给列表去重。用 set(列表) 转一下,重复的自动消失;想变回列表,再 list() 转回来。

"找重复数字"有个巧妙的思路:如果列表里有重复,那么"原列表长度"一定大于"去重后集合的长度"。长度对不上,就说明有重复:

nums = [3, 1, 4, 1, 5, 9, 2, 6, 5]

print(len(nums))        # 9(原列表)
print(len(set(nums)))   # 7(去重后)

判断"有没有重复",一行就行:

nums = [3, 1, 4, 1, 5, 9, 2, 6, 5]

if len(nums) == len(set(nums)):
    print("没有重复数字")
else:
    print("有重复数字!")

输出:

有重复数字!

但如果你想知道"哪些数字重复了",就得换招——遍历 + 计数:

nums = [3, 1, 4, 1, 5, 9, 2, 6, 5]
repeated = []                        # 装重复的数字

for n in nums:
    if nums.count(n) > 1 and n not in repeated:
        repeated.append(n)           # 出现超过 1 次,且还没记过
print(f"重复的数字有:{repeated}")     # [1, 5]

输出:

重复的数字有:[1, 5]

三个细节:set(列表) 只是"转过去去重",不改变原列表——想保留结果要重新赋值 nums = list(set(nums));去重后的集合顺序会变(集合无序),介意顺序就用"方法二"逐个找;动手前先想清楚要什么输出——是"有没有重复"(长度对比),还是"哪些重复了"(逐个找),两个问题答案不同。

重头戏:列表套字典

今天真正的核心:列表套字典——一个列表里每个元素都是一个字典。它就是一张 Excel 表格:

  • 整个列表 = 这张表
  • 列表里每个字典 = 一行记录(一个同学 / 一件商品 / 一条通讯录)
  • 字典的每个键 = 列名(姓名、成绩、电话……)
students = [
    {"name": "小明", "score": 92},
    {"name": "小红", "score": 85},
    {"name": "小刚", "score": 78},
]

把它当 Excel 看就通了:students[0] 是第一行,students[0]["name"] 是第一行的"姓名"这一格。以后所有项目的数据都长这样,今天必须亲手敲一遍。

:遍历每一行:

students = [
    {"name": "小明", "score": 92},
    {"name": "小红", "score": 85},
    {"name": "小刚", "score": 78},
]

for stu in students:                # stu 依次是每一行(一个字典)
    print(stu["name"], stu["score"])   # 从行里取列

输出:

小明 92
小红 85
小刚 78

取一格、改一格

students = [
    {"name": "小明", "score": 92},
    {"name": "小红", "score": 85},
    {"name": "小刚", "score": 78},
]

print(students[0])                  # {'name': '小明', 'score': 92}(第 1 行)
print(students[0]["name"])          # 小明(第 1 行的"姓名"列)

students[0]["score"] = 95           # 把小明(第 1 行)的成绩改成 95
print(students[0])                  # {'name': '小明', 'score': 95}

输出:

{'name': '小明', 'score': 92}
小明
{'name': '小明', 'score': 95}

:加一行用 append(字典),加一列是给某行加个键:

students = [
    {"name": "小明", "score": 92},
    {"name": "小红", "score": 85},
]

students.append({"name": "小丽", "score": 95})   # 新增一行
students[0]["city"] = "北京"                     # 给第一行加一列
print(students)

输出:

[{'name': '小明', 'score': 92, 'city': '北京'}, {'name': '小红', 'score': 85}, {'name': '小丽', 'score': 95}]

这里最常出的错,都在"层"上:

  • 下标和键要分清:students[0] 是第 1 行(字典),students[0]["name"] 才是一格(值),中间那层容易写漏。
  • 遍历变量 stu 是字典,stu["name"]取值,不是 stu[0]——很多人在这里用下标,会报 KeyError: 0
  • 加一行用 append(字典),加一列用 某行["新键"] = 值,两个动作别搞混。

表格的统计:按列算数

数据装进表格后,最常见的需求是"按列算数":把所有成绩取出来,求平均、最高、最低。做法是遍历每一行,把要算的那一列抽出来

students = [
    {"name": "小明", "score": 92},
    {"name": "小红", "score": 85},
    {"name": "小刚", "score": 78},
]

scores = []                        # 用来装抽出来的成绩
for stu in students:
    scores.append(stu["score"])    # 抽每一行的 score 列
print(scores)                      # [92, 85, 78]

print(f"平均分:{sum(scores) / len(scores)}")   # 平均分:85.0
print(f"最高分:{max(scores)}")                 # 最高分:92
print(f"最低分:{min(scores)}")                 # 最低分:78

输出:

[92, 85, 78]
平均分:85.0
最高分:92
最低分:78

"抽列"有更简短的写法——列表推导式(昨天看过,这里再用一次):

students = [
    {"name": "小明", "score": 92},
    {"name": "小红", "score": 85},
    {"name": "小刚", "score": 78},
]

scores = [stu["score"] for stu in students]    # 一行抽出所有成绩
print(scores)                                  # [92, 85, 78]
print(sum(scores) / len(scores))               # 85.0

输出:

[92, 85, 78]
85.0

"抽列"三步走:先建空列表 → 循环里 append 这一列 → 循环外用 sum / max / min 算。抽出来的 scores 是数字列表,才能用 sum / max / min;忘记抽、直接对字典列表用 sum 会报 TypeError。推导式看不懂没关系,明天的成绩管理会再用一次,看多了自然懂。

随堂练习

练习一(必做):找重复

给列表 [3, 1, 4, 1, 5, 9, 2, 6, 5],用集合判断有没有重复数字,再用方法二找出重复的分别是哪些。

提示:判断用 len(nums)len(set(nums)) 对比;找哪些重复用 for + count() + in 判断。

练习二:给成绩表加人改分

给 14.5 的 students 列表加 2 个新同学(append),把小明的成绩改成 95(students[0]["score"] = 95),再遍历打印全部。

提示:加人时注意格式——append({"name": "小丽", "score": 95}),花括号别丢。

练习三:算平均分

在练习二基础上,抽出所有成绩,打印平均分、最高分、最低分。

提示:scores = [stu["score"] for stu in students],然后 sum / max / min

进阶题(可选):购物车升级

把周二写的购物车升级——每个商品存一个字典(名字 + 价格),用户输入"商品名 价格"(用空格分开),输入 q 退出,最后遍历打印每个商品和总价。

提示:拆分输入用 split();数据结构是 cart = [{"name": "牛奶", "price": 3.5}, ...];总价用循环累加 total += item["price"]

写不出来时,让 AI 搭把手

场景:找重复数字写不出来,需要 AI 讲思路
提示词:
我是 Python 初学者,今天学集合。请帮我写一个程序:
列表 [3, 1, 4, 1, 5, 9, 2, 6, 5] 里,找出重复出现的数字。
先别给完整代码,先给我讲思路:怎么用集合判断有没有重复,
再告诉我用什么方法找出来。我是初学者,用大白话讲。
场景:列表套字典的某个操作不会写,比如"给表格加一行"
提示词:
我有这样一个列表套字典的数据:
students = [{"name": "小明", "score": 92}, {"name": "小红", "score": 85}]
我想:1. 加一个新同学 2. 修改小明的成绩 3. 遍历打印每个人的姓名和成绩。
请给我完整的示例代码,每行加注释,用最简单的写法。

常见报错速查

报错信息 什么意思 怎么办
TypeError: unhashable type: 'list' 往集合里放列表 / 拿列表当键 集合元素和字典键只能用"简单值"(数字、字符串)
TypeError: 'set' object is not subscriptable 给集合按下标 [0] 取值 集合没有顺序,用 in 判断或用 for 遍历
AttributeError: 'set' object has no attribute 'append' 集合没有 append 方法 集合加元素用 add
KeyError: 0 遍历列表套字典时用了下标取列 字典取值得用键:stu["name"],不是 stu[0]

收个尾

今天两个内容,一个轻一个重:集合自动去重,set(列表) 转一下重复全没,加元素用 add(没有 append),交集 &、并集 | 会看就行——这些是轻的。重的是列表套字典:列表 = 行、字典 = 一行、键 = 列名,一张数据表格。表格三件套动作要练熟:加行 append(字典)、改格 某行["键"] = 新值、抽列 [行["键"] for 行 in 表格]

明天是本周综合练习:用今天学的列表套字典和前面的知识,完成两个完整程序——词频统计(统计一句话里每个词出现几次)和成绩管理(算平均分、最高分、最低分),全程 AI 协作,把这周学的东西真正串起来。