容器集合 课件
引言
在 1.1 对象与类型 里,每种类型都只装一个值。然而,一门编程语言真正的威力,来自它把许多值聚到一起、并把整组当作一个东西来对待的能力——一份学生名册、一张图像的像素、一张测量数据表。做到这一点的类型就是容器(container),它们是大多数真实 Python 程序耗费时间的地方。
本页介绍三大容器家族,区分它们的问题是元素是怎么存放的? 序列(sequence)按顺序保存元素,因此每个元素都有一个可供索引的位置。集合(set)只保存不重复的元素,丢弃重复项、也不记顺序。映射(mapping)把每个元素与一个键配对,于是你按名字而非位置来查找。三者都见过之后,第 4 节会回到 1.1 的第二条轴——可变性——因为它悄悄支配着它们的行为;第 5 节再讲切片,一次读取整段子序列。本页代码可运行,请边读边试。
1. 序列:列表、元组与 range 课件
你最常用到的容器是有序的序列。两位主力是列表(list)和元组(tuple)。两者都保存一组有序的元素;最关键的区别在于可变性。列表用方括号 [] 写出,是可变的;元组用圆括号 () 写出,是不可变的。
下面的示例各建一个并打印出来——注意两者都能混放不同类型的元素。
示例:一个列表和一个元组
第三种序列 range 表示一个整数的等差数列,却并不把它们全部存下来——range(1, 101) 代表 1 到 100,但只保存起点、终点和步长。它是生成数字序列的惯用方式,常常直接交给 list() 或 tuple()。
示例:用 range 生成数字
range 最多接受三个参数——range(stop)、range(start, stop)、range(start, stop, step)——而且和切片一样,stop 是被排除的。负步长向下计数,空 range 也完全合法。
示例:range 的几种形式
课堂练习:构建序列
- 创建一个包含若干不同类型元素的列表;打印整个列表,并用索引打印其中一个元素(索引从 0 开始)。
- 对元组做同样的事。
- 用
range()构建一个包含 1 到 100 的列表,再构建一个这样的元组。
序列共享一套通用工具——索引、用 in 做成员检查、用 len() 取长度,以及切片(第 5 节)。它们还有方法(method),即附着在对象上、用点号调用的函数。
核心概念:方法
方法是隶属于某个对象、并以点号在其上调用的函数,例如 my_list.append(4)。由于
列表是可变的,许多列表方法会原地修改列表;元组是不可变的,因此没有这类方法。
大多数列表方法原地改动列表——它们修改调用者本身,并且通常返回 None 而不是一个新列表。这一点值得说两遍,因为每个人都会栽一次:l.append(4) 给你的是 None,不是那个列表。
你会一直用到的有四个:append()(在末尾添加一个元素)、extend()(把另一个可迭代对象的所有元素加进来)、reverse()(就地反转顺序)和 pop()(移除并返回某个索引处的元素,默认是最后一个)。
示例:四个列表方法
l = [1, 2, 3]
l.append(4) # 加进来一个元素
print(l) # [1, 2, 3, 4]
l.extend([5, 6]) # 实参的每一个元素都被加进来
print(l) # [1, 2, 3, 4, 5, 6]
l.reverse() # 原地反转;返回 None
print(l) # [6, 5, 4, 3, 2, 1]
last = l.pop() # 移除并“返回”最后一个元素
first = l.pop(0) # ……或者某个索引处的元素
print(last, first, l) # 1 6 [5, 4, 3, 2]
print(l.append(9)) # None —— 这个方法改动了 l,什么也没返回
append 和 extend 的差别是最需要留神的:append 把它的实参当作单独一个元素加进来,所以 l.append([5, 6]) 会把一个列表塞进你的列表里,而 extend 会把它拆开。两个都试一遍,比较一下。
易错点:(5) 不是元组
l.extend((5)) 会失败,报 TypeError: 'int' object is not iterable。(5) 里的
括号只是用来分组的——它并不造出一个元组。造出元组的是那个逗号:(5,) 才是
一个单元素元组,l.extend((5,)) 就能用。这也正是 type((5)) 是 int、而
type((5,)) 是 tuple 的原因。
现在把列表和元组并排放着看。凡是只读取的操作两者都行;凡是要改动的只有列表行。整张表都由这一条规则生出来。
| 操作 | l = [1, 2, 3] |
t = (4, 5, 6) |
为什么 |
|---|---|---|---|
l[1] —— 按索引读取 |
✅ | ✅ | 读取从不改动 |
1 in l —— 成员检查 |
✅ | ✅ | 读取 |
len(l) |
✅ | ✅ | 读取 |
l[1] = 99 —— 赋值 |
✅ | ❌ TypeError |
改动了对象 |
del l[1] |
✅ | ❌ TypeError |
改动了对象 |
.append() .extend() .reverse() .pop() |
✅ | ❌ AttributeError |
元组没有这些方法 |
l += [6, 7] |
✅ id 不变 |
✅ id 改变 |
见第 4 节 |
请仔细看最后一行:+= 对两者都合法,但对各自意味着不同的事情。对列表,它原地改动那个对象,所以标识不变;对元组,它只能造出一个全新的对象。第 4 节会回到这一点,因为它最清楚地展示了可变性到底意味着什么代价。
2. 集合 课件
如果说序列精心记着元素的顺序,那么集合则刻意把顺序忘掉。集合是一组互不相同的元素的无序汇集,与数学中的集合相呼应:{1, 2} 和 {2, 1, 1} 是同一个集合。集合是可变的,它不可变的对应物是 frozenset。它擅长两件事:快速的成员检查与去重。
下面的示例一次展示这两件事,以及如何从另一个集合体构建集合。
示例:创建集合与去重
因为集合与数学集合相呼应,它支持那些熟悉的集合运算,每一种都既有方法形式、也有运算符形式。
示例:集合运算
集合的元素必须是可哈希的(hashable),这正是集合能装数字、字符串、元组、却不能装列表的原因。当你需要一个不可变的集合——例如用作字典的键或另一个集合的成员——就用 frozenset。
深入了解:“可哈希”是什么意思?
一个对象如果有一个在其生命周期内永不改变的哈希值,它就是可哈希的;正是这一点让
Python 能把它放进那张令集合成员检查和字典查找飞快的内部表里。不可变的内置类型(数字、
字符串、只含可哈希元素的元组)是可哈希的;可变的(列表、字典、集合)则不是——这正是
为什么 list 不能做集合元素或字典键,而 frozenset 可以。这就是第 4 节那条
可变/不可变界线的实际回报。
课堂练习:集合
- 从
numbers = [1, 2, 2, 3, 4, 4, 5]得到一个只含唯一值的列表。 - 用
a = {1, 2, 3, 4}与b = {3, 4, 5, 6},求出在a或b、但不同时在两者中的值。 - 试着把一个列表加入集合,读读那个错误;再换成元组试试,看看它能成功。
3. 映射:字典 课件
三大家族中的最后一个干脆抛开位置,转而把每个值与一个你自定的键配对。字典(dict)保存键—值对,是日常 Python 中最重要的容器。它是可变的,让你按键、而非按位置来查找一个值。
下面的示例用两种方式创建字典,查找一个值,并分别添加和更新一个键值对。
示例:创建并使用字典
用 [] 查找一个不存在的键会引发 KeyError。当某个键可能不在时,get() 返回 None(或你给定的默认值)而不会崩溃——而 update() 则把另一个字典合并进来。
示例:安全查找与合并
要遍历字典,就迭代它的 keys()、values() 或 items()——最后一个会把键和值一并交给你,等你在 1.3 用到 for 循环时会不断用到它。
易错点:字典的键必须可哈希
键必须是可哈希的(见集合里的“深入了解”),所以你可以用数字、字符串或元组作键—— 但绝不能用列表。而值则可以是任何对象。
课堂练习:字典
- 建一个把三个名字映射到年龄的字典,然后打印某个人的年龄。
- 用
get()查找一个不存在的名字,返回"unknown"而不崩溃。 - 添加一个新人,再更新一个已有的人的年龄。
3.1 字典来自文件:JSON
字典的意义不止于你自己的代码,因为程序之间交换结构化数据最常用的那种格式,看上去几乎就是一部字典。JSON(JavaScript Object Notation)是由名称和值搭起来的纯文本,而标准库能把它直接读成一个 dict。
这里出现了两样新语法,两样都值得现在就认识。open() 给你一个文件对象,而 with 语句保证它会被重新关上——哪怕中途出错也一样。写成 with open(...) as f: 就是 Python 里打开文件的方式;不用 with 打开又忘了关,是一个真实存在、只是发作得慢的 bug。
假设 settings.json 的内容是:
那么:
import json
with open("settings.json", "r") as f: # 'with' 替你关掉这个文件
setting_dict = json.load(f) # JSON 文本 -> 一部真正的 dict
print(setting_dict)
print(setting_dict["year"]) # 2026
print(setting_dict.items()) # dict_items([...]) —— 键值对
结果就是一部普通的字典;json.load 并没有自己的什么特殊类型。也请注意,嵌在里面的 ["objects", "collections"] 变成了一个普通的 Python 列表,所以整个结构都是由你已经认识的那些容器搭起来的。
最后那一行有点意思。setting_dict.items() 打印出来并不像一个列表——它显示成 dict_items([...])。它是一个视图(view):一扇朝着这部字典的活窗口,而不是它的一份副本。改动这部字典,视图也跟着变。这是你第一次瞥见一条贯穿 1.4 的线索——Python 常常交给你一个描述数据的东西,而不是数据的第二份拷贝。
示例:不用文件的 json
这一页上的可运行单元没有文件系统可读,所以请用 json.loads(带一个 s,表示
string)来解析你手里已有的文本。结果和 json.load(f) 会给你的完全一样:
import json
text = '{"course": "Python", "year": 2026, "topics": ["objects", "collections"]}'
setting_dict = json.loads(text)
print(type(setting_dict)) # <class 'dict'>
print(setting_dict["topics"]) # ['objects', 'collections']
print(setting_dict.items())
setting_dict["year"] = 2027 # 视图会反映这次改动
print(setting_dict.items())
4. 可变性与标识 课件 课件
我们在 1.1 的两条轴中已经见过可变性;现在手里有了容器类型,可以好好讲讲它了,因为它悄悄支配着赋值、比较、乃至字典键的行为。
列表与元组之间那道根本性的区别,会在好几种操作里显现出来。下面的示例表明:两者都能按索引读取元素,但只有列表允许你重新赋值或删除元素。(那行会在元组上失败的语句被注释掉了——取消注释即可看到错误。)
示例:只有列表能被修改
可变性还解释了 += 的一个微妙行为。下面的示例表明:对可变的列表,+= 原地修改对象,因此其标识不变;对不可变的元组,+= 必须构造一个全新的对象,因此其标识改变。
示例:+= 与对象标识
易错点:两个名称,同一个可变对象
因为名称只是一张标签,b = a 会让两个名称指向同一个对象。如果那个对象是可变的,
通过一个名称所做的改动,会从另一个名称看到:
这种别名(aliasing)对不可变对象无害(你本就无法改变它们),却是可变对象的一大 经典 bug 来源。
从图上看,两个名称共享同一个列表,因此通过 b 所做的修改会从 a 看到:
这里还有一个实际的回报:正是不可变性,让一个对象能被用作字典的键或集合的成员。试着拿一个列表当键,Python 会拒绝,因为键不能在字典脚下悄悄改变。
深入了解:可变/不可变的界线是本质的吗?
一半是。在语言层面,可变性是一个真实、可观察的契约,每一种 Python 实现都遵守它, 也正是它使得不可变对象可被哈希(从而能当字典键或集合成员)。但这条界线也有诚实的 灰色地带。元组是不可变的,可是一个装着列表的元组,却允许你修改里面的那个列表—— 元组自身的引用没有变,但引用所指向的东西可以变。而且,一个类型之所以不可变,往往是 一种设计与实现上的取舍(安全、共享、优化、哈希),而非什么深刻的定律。所以:把可变性 当作一个真实而有用的概念,但要明白这条线是 Python 的设计者画下的,底层有 C 语言层面的 机制在支撑——并非由数学颁布。
课堂练习:可变性实践
- 把一个列表的第二个元素设为
-1,确认它成功;再对一个元组做同样的事,读读那个错误。 - 用
id()查看一个列表在原地修改前后的标识——它保持不变吗?然后用一句话解释:为什么对元组的“修改”需要一个新对象。
5. 切片:读取子序列 课件
索引读取一个元素;切片(slicing)读取一整段子序列。其写法是 sequence[start:stop:step],其中 start 是要包含的第一个索引(默认 0),stop 是要排除的第一个索引,step 是步长(默认 1;负步长表示反向)。冒号是必需的,三个数字各自可选。
下面的示例用几种方式对一个列表切片——改改数字再运行,建立直觉。
示例:对列表切片
课堂练习:这些是切片吗?
对下面每一项,判断它写成 seq[…] 时是否是一个有效的切片。如果是,说出它选中了
哪些元素;如果不是,说明问题出在哪里。先自己推一遍,再用一个真实的列表
(比如 seq = list(range(10)))验证。
1:2:12:4:79:1:-1a:b:c1.5:2.3:3.14a:2:36:7:-5:-1::-1
其中有两项,取决于语法之外的某样东西——说说是什么。
深入了解:切片本身也是一个对象
写法 start:stop:step 会构造一个 slice 对象,你也可以显式地创建并复用它:
所以 slice 是一种类型,而 xs[1:10:2] 是 xs[slice(1, 10, 2)] 的语法糖。
小结
容器是 Python 程序存放数据的地方。把任何内置容器放到 1.1 的两条轴上——它装什么、它能不能变:
| 类别 | 类型 | 可变? |
|---|---|---|
| 序列 | list / tuple、range |
list 可变,tuple/range 不可变 |
| 集合 | set / frozenset |
set 可变,frozenset 不可变 |
| 映射 | dict |
可变 |
可变性不只是一个标签:它解释了别名现象、+= 的行为,以及为什么只有不可变(可哈希)的对象才能当字典键或集合成员。有了类型(1.1)与容器(1.2)在手,1.3 控制流程 将让它们动起来——遍历它们、检验它们。