当前位置:首页>python>Python入门——容器四件套

Python入门——容器四件套

  • 2026-10-11 05:45:20
Python入门——容器四件套

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
索引超出范围(比如空列表取 [0])
TypeError: 'list' object is not callable
把列表当函数调用(fruits())
list.remove(x)
 不报错也不删
找不到 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:

  • ==
     比的是“值是否相等”
  • is
     比的是“是不是同一个对象(内存地址相同)”
a = [1, 2, 3]
b = [1, 2, 3]
print(a == b)  # True(值相等)
print(a is b)  # False(是两个不同的列表对象)

经验法则:比较值用 ==,比较身份用 is(后者最常见的场景是和 None 比较:if x is None:)。

🎯 动手练

写一个迷你“学生成绩管理系统”:

  1. 用字典存学生信息,键是学生姓名,值是成绩列表
  2. 实现 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
  • 集合运算 & | -,in 判断包含比列表快得多
  • 浅拷贝陷阱
    :b = a 不是拷贝是共享,要用 .copy() / list(a) / a[:]
  • == 比值,is 比对象
    ;默认用 ==,只和 None 比较时用 is
  • 嵌套结构(列表里装字典、字典里装列表)对应 JSON,是真实项目的常态

最新文章

随机文章