4. 一组数据放一起——列表 / 元组 / 字典 / 集合
读完本节,你能在 25 分钟内,掌握 4 种 Python 内置容器,并能根据“要不要改 / 怎么找”正确选用,主动避开浅拷贝和 == vs is 这两个隐蔽坑。
前面的变量只能装一个值,但现实中“一组东西”很常见:购物清单、通讯录、班级成绩。Python 给了我们 4 种容器,各有用途。
4.1 为什么需要容器?
如果不用列表,记录 3 个学生成绩要写 3 行:
score1 = 90
score2 = 80
score3 = 70
print((score1 + score2 + score3) / 3) # 算平均分
如果学生多了,根本写不过来。用列表一行搞定:
scores = [90, 80, 70]
print(sum(scores) / len(scores)) # 算平均分:80.0
4.2 列表 list:可变、有序
列表是最常用的容器,用方括号 [] 定义:
fruits = ["apple", "banana", "cherry"] # 定义
print(fruits[0]) # 正向索引从 0 开始:apple
print(fruits[-1]) # 负向索引从 -1 开始:cherry
print(fruits[1:3]) # 切片:[banana, cherry] (含左不含右)
列表里的元素可以是任何类型,也可以混合:
mixed = [1, "hello", 3.14, True, [1, 2]] # 一个列表装 5 种类型
增删改查全功能:
fruits = ["apple", "banana"]
fruits.append("cherry") # 末尾追加
fruits.insert(0, "apricot") # 在索引 0 位置插入
fruits.extend(["date", "fig"]) # 拼接另一个列表
print(fruits) # ['apricot', 'apple', 'banana', 'cherry', 'date', 'fig']
fruits.remove("banana") # 按值删第一个匹配项
popped = fruits.pop() # 弹掉最后一个,返回值是被弹掉的
del fruits[0] # 按索引删
print(fruits) # ['apple', 'cherry', 'date']
fruits[0] = "avocado"# 改
print("apple"in fruits) # 查:False
print(fruits.index("cherry")) # 查索引:1
排序两个常用方法:
nums = [3, 1, 4, 1, 5, 9, 2, 6]
print(sorted(nums)) # [1, 1, 2, 3, 4, 5, 6, 9] 返回新列表
print(nums) # 原列表没变:[3, 1, 4, 1, 5, 9, 2, 6]
nums.sort() # 原地排序,直接改 nums
print(nums) # [1, 1, 2, 3, 4, 5, 6, 9]
排序还能传 key 参数定制规则:
words = ["banana", "pie", "Apple"]
print(sorted(words)) # ['Apple', 'banana', 'pie'](默认按字符编码)
print(sorted(words, key=str.lower)) # ['Apple', 'banana', 'pie'](忽略大小写)
print(sorted(words, key=len)) # ['pie', 'Apple', 'banana'](按长度)
列表推导式(list comprehension)是把 for 循环塞进一行的写法:
nums = [1, 2, 3, 4, 5]
squares = [n * n for n in nums] # [1, 4, 9, 16, 25]
evens = [n for n in nums if n % 2 == 0] # [2, 4]
初学阶段可以先记“循环能做的一切,列表推导式都能写得更短”。
4 个常见错误:
| |
|---|
IndexError: list index out of range | |
TypeError: 'list' object is not callable | |
list.remove(x) | |
del lst[i] | |
初学期间,看到报错先看最后一行“Error 类型 + 提示”,90% 的问题直接能定位。
4.3 元组 tuple:不可变、有序
元组像“加锁的列表”——一旦定义,不能增删改。用圆括号 () 定义:
point = (3, 5) # 一个坐标
colors = ("red", "green", "blue")
print(point[0], point[1]) # 3 5
“不可变”指的是不能改元素本身,但元素如果是可变对象(比如列表),那个内部列表还能改:
t = (1, [2, 3], 4)
t[1].append(5) # 元组本身没变,但内部列表加了 5
print(t) # (1, [2, 3, 5], 4)
元组比列表快(因为不可变,内存分配更紧凑),适合存“只读”的数据;也可以作为字典的键(S4.4 节会用到)。
关键特性:解包(unpacking):
x, y = (3, 5) # 把元组拆成两个变量
print(x, y) # 3 5
# 星号收尾,吸走剩下的所有项
first, *middle, last = [1, 2, 3, 4, 5]
print(first, middle, last) # 1 [2, 3, 4] 5
用解包交换两个变量:
a, b = 3, 5
a, b = b, a # 不用中间变量,一行搞定交换
print(a, b) # 5 3
🐛 避坑:单元素元组必须带逗号 —— (3) 只是括号里的 3(整数),(3,) 才是元组。这一条新手踩过无数遍。
print(type((3))) # <class 'int'>
print(type((3,))) # <class 'tuple'>
元组适合存“不该被改”的数据:坐标、身份证号、函数返回多个值:
defget_min_max(numbers):
returnmin(numbers), max(numbers) # 其实是返回一个元组
lo, hi = get_min_max([3, 1, 2]) # 直接解包
print(lo, hi) # 1 3
💡 小贴士:看到 def foo(): return a, b 不用懵 —— 这就是返回元组 (a, b) 的语法糖。Python 大量内置 API 都用“返回元组”代替“返回自定义对象”,比如 divmod(10, 3) 返回 (3, 1),os.path.split("/a/b/c") 返回 ("/a/b", "c")。
4.4 字典 dict:键值对
字典是通讯录的天然模型 —— 名字找电话:
user = {"name": "小明", "age": 25, "city": "杭州"}
print(user["name"]) # 小明
user["email"] = "x@x.com"# 加键值对
del user["age"] # 删
print(user) # {'name': '小明', 'city': '杭州', 'email': 'x@x.com'}
字典的键必须是不可变类型(数字 / 字符串 / 元组都行,列表 / 字典 / 集合都不行):
d = {1: "一", "name": "小明", (1, 2): "坐标"}
# d = {[1, 2]: "错误"} # TypeError: unhashable type: 'list'
🐛 避坑:直接 user["phone"] 取不存在的键会抛 KeyError。用 .get(key, default) 防报错:
print(user.get("phone", "未知")) # 未知
print(user.get("phone")) # None(默认返回值)
遍历字典的 3 种姿势:
for k in user.keys(): # 只拿键
print(k)
for v in user.values(): # 只拿值
print(v)
for k, v in user.items(): # 同时拿键值(最常用)
print(f"{k} = {v}")
字典推导式(dict comprehension):
names = ["小明", "小红", "小刚"]
scores = [85, 92, 78]
score_dict = {n: s for n, s inzip(names, scores)}
# {'小明': 85, '小红': 92, '小刚': 78}
字典和 JSON 的关系非常近 —— JSON 本质上就是“嵌套的字典 / 列表”。第 8 节会用 JSON 持久化数据。
4.5 集合 set:不重复、无序
集合像“去重过的列表”,用花括号 {} 定义(注意空集合必须用 set(),不是 {}):
tags = {"python", "tutorial", "python"} # 自动去重
print(tags) # {'tutorial', 'python}(顺序不固定)
nums = set([1, 2, 2, 3, 3, 3])
print(nums) # {1, 2, 3}
集合运算:
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}
print(a & b) # 交集:{3, 4}
print(a | b) # 并集:{1, 2, 3, 4, 5, 6}
print(a - b) # 差集:{1, 2}(a 有 b 没有的)
print(3in a) # 判断包含:True(比列表快非常多)
一个真实场景:找出两个用户列表的“同时关注”、“只关注 A”、“全部关注”:
user_a = {"python", "机器学习", "摄影"}
user_b = {"python", "摄影", "篮球"}
print("共同兴趣:", user_a & user_b) # {'python', '摄影'}
print("只 A 有:", user_a - user_b) # {'机器学习'}
print("只 B 有:", user_b - user_a) # {'篮球'}
print("所有兴趣:", user_a | user_b) # {'python', '机器学习', '摄影', '篮球'}
集合推导式:
nums = [1, 2, 2, 3, 3, 4]
unique = {n for n in nums}
print(unique) # {1, 2, 3, 4}
💡 小贴士:如果你只关心“某个东西在不在”,用集合;如果还要保留顺序,用列表。集合的 in 判断是 O(1),列表的 in 是 O(n),数据量大时差异明显。性能数据:列表 100 万元素 in 判断 ~5ms,集合 < 0.01ms。
4.6 容器互转与嵌套
list((1, 2, 3)) # (1,2,3) → [1, 2, 3]
tuple([1, 2, 3]) # [1,2,3] → (1, 2, 3)
set([1, 2, 2, 3]) # [1,2,2,3] → {1, 2, 3}
list({1: "a", 2: "b"}.keys()) # [1, 2]
嵌套:列表里装字典做待办清单:
todos = [
{"id": 1, "title": "买菜", "done": False},
{"id": 2, "title": "写代码", "done": True},
]
# 加一条
todos.append({"id": 3, "title": "运动", "done": False})
# 查未完成的
print([t["title"] for t in todos ifnot t["done"]])
# 输出:['买菜', '运动']
更深一层的嵌套:字典里装列表、列表里装字典都很常见。比如“班级-学生-成绩”:
school = {
"class_1": [
{"name": "小明", "scores": [85, 90, 78]},
{"name": "小红", "scores": [92, 88, 95]},
],
"class_2": [
{"name": "小刚", "scores": [76, 80, 72]},
],
}
# 查 class_1 的平均分
scores = [s for stu in school["class_1"] for s in stu["scores"]]
print(sum(scores) / len(scores)) # 88.0
这种嵌套结构对应 JSON 格式,是 Web API 返回数据的标准形式。第 8 节读写 JSON 时会再次遇到。
4.7 浅拷贝陷阱与 == vs is
最后讲两个隐蔽但重要的区别。
浅拷贝陷阱:直接赋值不是拷贝,是共享引用:
a = [1, 2, 3]
b = a # b 和 a 指向同一个列表
b.append(4)
print(a) # [1, 2, 3, 4] ← a 也变了!
要拷贝,得显式调用:
a = [1, 2, 3]
b = a.copy() # 或 b = list(a) 或 b = a[:]
b.append(4)
print(a) # [1, 2, 3] ← a 不受影响
print(b) # [1, 2, 3, 4]
深拷贝(嵌套列表也要复制):
import copy
a = [[1, 2], [3, 4]]
b = copy.deepcopy(a)
b[0].append(99)
print(a) # [[1, 2], [3, 4]] ← 深拷贝,a 不受影响
print(b) # [[1, 2, 99], [3, 4]]
如果只用 .copy()(浅拷贝),嵌套列表还是共享的:
a = [[1, 2], [3, 4]]
b = a.copy()
b[0].append(99)
print(a) # [[1, 2, 99], [3, 4]] ← 嵌套部分还是共享!
📌 对比 == vs is:
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b) # True(值相等)
print(a is b) # False(是两个不同的列表对象)
经验法则:比较值用 ==,比较身份用 is(后者最常见的场景是和 None 比较:if x is None:)。
🎯 动手练
写一个迷你“学生成绩管理系统”:
- 实现 3 个查询(用函数也行,先 print 也行):
预期数据结构:
students = {
"小明": [85, 90, 78],
"小红": [92, 88, 95],
"小刚": [76, 80, 72],
}
练习参考答案
students = {"小明": [85, 90, 78], "小红": [92, 88, 95]}
print("小明的平均分:", sum(students["小明"]) / len(students["小明"]))
all_avg = sum(sum(s) for s in students.values()) / sum(len(s) for s in students.values())
print("全班平均分:", all_avg)
# lambda 用法见 S6.5 —— 这里先用着,语义是"按每名学生最高分排序找最高的学生"
top = max(students, key=lambda n: max(students[n]))
print("最高分学生:", top)
本节小结
- 列表
[] 可变有序、元组 () 不可变有序、字典 {键:值} 键值对、集合 {} 不重复无序 - 列表切片:正向从 0、负向从 -1、
[start:stop:step],[::-1] 反转 - 单元素元组必须
(3,) 带逗号;解包让函数返回多值更自然 - 字典的键必须是不可变类型(数字 / 字符串 / 元组)
- 字典
.get(key, default) 防 KeyError - 浅拷贝陷阱:
b = a 不是拷贝是共享,要用 .copy() / list(a) / a[:] == 比值,is 比对象- 嵌套结构(列表里装字典、字典里装列表)对应 JSON,是真实项目的常态