内置容器、切片、解包与复制语义
本节目标
查询内置容器、切片、解包、可变性和复制。
选择容器时,先确定数据按位置、键还是唯一性组织,再考虑是否需要修改。本章把上一章的对象、名称与可变性模型用于列表、元组、字典和集合,并沿着索引、切片、解包与复制追踪新对象和共享引用。第一次接触这些容器时,可从任务地图直接建立选择依据。
内置容器任务地图
“数据靠什么被找到”通常是选择容器的第一问;同一份业务数据在不同处理阶段也可能采用不同表示。
| 任务特征 | 常用容器 | 关键性质 |
|---|---|---|
| 按位置保存、允许重复且需要增删 | list | 有顺序、可变的序列 |
| 按位置保存、结构固定 | tuple | 有顺序、不可变的序列 |
| 表示一段按步长生成的整数 | range | 不可变的整数序列表示 |
| 按键查找值 | dict | 保留插入顺序的可变映射 |
| 去重、成员判断或集合运算 | set | 可变、元素唯一且必须可哈希 |
| 需要不可变的集合值 | frozenset | 不可变、元素唯一且必须可哈希 |
表中的“有顺序”只表示迭代或按位置观察时存在规定的先后关系,不代表内容已按大小或业务含义排序。是否允许重复、是否按键访问,以及容器自身是否需要修改,仍要分别判断。
列表与可变序列操作
列表字面量用方括号创建,例如 names = ["Ada", "Guido"]。列表是可变序列:append(value) 在末尾追加一个元素,extend(iterable) 逐个追加可迭代对象产生的元素,insert(index, value) 在指定位置插入;下标赋值、切片赋值和 del 也会修改原列表。
列表方法不能一概视为“返回修改后的列表”。items.append(value)、items.extend(values) 和 items.sort() 都修改原对象并返回 None,所以 items = items.append(value) 会把名称改绑到 None。相比之下,items + other 和普通列表切片会创建新列表。遍历期间改变列表长度可能跳过元素;筛选操作应先明确结果容器,推导式语法留待后续章节。
列表保存的是对象引用,而不是把每个对象递归嵌入一份。于是列表可变并不代表元素都可变,列表不可作为集合元素或字典键,也不代表从列表得到的所有副本都会递归复制元素。
元组、逗号与 range
元组由逗号形成,括号主要用于分组:pair = 10, 20 与 pair = (10, 20) 都产生二元组。单元素元组必须保留逗号,写作 (10,);(10) 只是整数表达式。元组不可变,不能替换、插入或删除其中的位置,但元组中的引用仍可能指向可变对象;修改那个对象不等于修改元组的元素绑定。
range(stop)、range(start, stop) 和 range(start, stop, step) 表示按规则产生的整数序列,终点 stop 不包含在内,step 不能为零。range(1, 7, 2) 依次表示 1、3、5。range 本身不可变,也不会为了表示很大的区间而预先建立一张完整整数列表;需要列表时可显式调用 list(...)。
位置数量和含义固定的记录可用元组,整数区间可用 range,需要更新的通用序列则通常使用列表。元组和 range 的不可变性只约束容器自身提供的更新操作,不会递归传递给所有成员。
索引、负索引与切片
序列索引从 0 开始。对长度为 n 的序列,有效非负索引是 0 到 n - 1;负索引从末尾计数,-1 是最后一个元素,-n 是第一个元素。单个索引超出有效范围会抛出 IndexError,空序列没有任何有效索引。
切片 sequence[start:stop:step] 使用半开区间,包含 start 所指位置而不包含 stop。在默认正步长下,numbers[1:3] 从 [10, 20, 30, 40] 得到 [20, 30],边界也可省略为 numbers[:2] 或 numbers[2:]。超出范围的切片边界会被调整到可用范围,因此 numbers[:100] 合法并取得全部元素;单个索引越界则会报错。
step 控制间隔和方向,但不能为零。负步长既反转行进方向,也改变省略边界的默认值,例如 numbers[::-1] 会得到逆序的新列表。列表切片虽然创建新外层容器,其中保存的仍是原有元素引用;字符串和元组切片则返回相应序列类型。新切片不等于成员已被递归复制。
字典、键与插入顺序
字典把可哈希的键映射到值,例如 metadata = {"language": "python", "version": "3.14"}。metadata["language"] 读取已有键;对缺失键使用方括号会抛出 KeyError。只想提供缺失时的默认值可用 metadata.get("status", "unknown"),而赋值 metadata["status"] = "draft" 会新增或更新条目。
迭代字典键时可依赖插入顺序:更新已有键不会改变它的位置,删除后重新加入则形成一次新插入。这项保证仅覆盖条目的观察次序,不表示键已按字母、数值或业务含义排列。需要特定顺序时,应显式调用 sorted(...) 并提供合适依据。
键必须可哈希,并且在参与哈希期间保持适合相等比较的稳定状态。字符串、整数以及成员均可哈希的元组常可作键,列表和字典不可作键。值没有这个限制,可以是列表等可变对象。
set、frozenset 与集合运算
set 保存互不重复的可哈希元素,适合成员判断、去重和集合关系运算。left | right 求并集,left & right 求交集,left - right 求差集,left ^ right 求对称差集。空集合必须写 set();{} 创建的是空字典。向集合加入列表会因列表不可哈希而抛出 TypeError。
set 提供 add()、remove()、discard() 等修改操作;frozenset 不可变,在成员均满足要求时还可作为字典键或另一个集合的元素。两者表达集合关系,均不承诺可依赖的元素迭代顺序。某次运行中看似固定的打印次序,不应进入协议、测试或教材结论。
稳定展示集合时,可以先用 sorted(topics) 取得列表。排序本身要求元素之间存在合适关系;若混合类型不能直接比较,就要定义业务排序键。本文的代表脚本同样只输出排序后的集合值。
打包、解包与星号目标
逗号可以把多个值打包成元组,也可以让赋值目标按位置解包:x, y = 10, 20 先产生右侧值,再分别绑定名称,因此 x, y = y, x 可以交换绑定。没有星号目标时,目标数量必须与输入元素数量完全相同,否则抛出 ValueError。
一个赋值目标列表中最多有一个星号目标。head, *middle, tail = [10, 20, 30, 40] 令 head 为 10、middle 为新列表 [20, 30]、tail 为 40。星号目标可以接收零个元素,但其余非星号目标仍必须得到值;输入过短仍会抛出 ValueError。
解包逐个接收输入产生的对象引用,不会递归复制它们;嵌套目标还要求输入具备对应的嵌套结构。函数调用也使用 *iterable 与 **mapping 展开数据,相关参数绑定规则将在函数章节说明。
别名、浅复制与嵌套共享
赋值 alias = original 只增加一个指向同一对象的名称,alias is original 为真;通过任一名称修改这个可变对象,另一名称都会看到变化。outer_copy = original.copy() 对列表创建一个新的外层列表,所以两个外层列表身份不同,但新列表中的元素仍是原列表所引用的那些对象。
元素本身是可变容器时,修改共享的内层对象会同时反映在两个外层列表中。浅复制的准确含义是复制一层容器结构、复用成员引用,而非递归复制;.copy()、切片或 list(original) 都不能证明嵌套结构已经彼此独立。只有确实需要隔离全部嵌套状态时,才有必要设计递归构造或评估深复制的语义。
运行代表脚本时,可以依次观察切片与解包、字典插入顺序、排序后的集合输出,以及浅复制后仍被两个外层列表共享的内层列表。
numbers = [10, 20, 30, 40]
head, *middle, tail = numbers
metadata = {"language": "python", "version": "3.14"}
topics = {"typing", "python"}
nested = [["draft"]]
outer_copy = nested.copy()
outer_copy[0].append("review")
print(f"slice={numbers[1:3]}")
print(f"unpacked=head:{head},middle:{middle},tail:{tail}")
print(f"dict-order={list(metadata)}")
print(f"set-values={sorted(topics)}")
print(f"outer-copy-shared={nested[0] is outer_copy[0]}")
print(f"inner-values={nested[0]}")
slice=[20, 30]
unpacked=head:10,middle:[20, 30],tail:40
dict-order=['language', 'version']
set-values=['python', 'typing']
outer-copy-shared=True
inner-values=['draft', 'review']
脚本中的 nested is outer_copy 为假,而 nested[0] is outer_copy[0] 为真。对 outer_copy[0] 的追加修改的是共享内层列表,所以从 nested[0] 也能读到 "review";这不是两个独立内层列表自动同步。
容器选择、复制与遍历边界
- 混淆索引与切片边界:单个越界索引抛出
IndexError,越界切片边界通常被调整;切片终点仍不包含在结果中。 - 假定字典自动按键排序:字典保留插入顺序,不承诺任意业务排序;需要排序时显式表达排序依据。
- 直接展示集合并依赖其顺序:
set和frozenset不提供稳定迭代顺序;固定输出应先sorted(...)。 - 用方括号读取可能缺失的字典键:它会抛出
KeyError;根据业务需要选择成员检查、get(),或明确处理异常。 - 把不可哈希对象作为键或集合元素:列表、字典和集合本身不可哈希,会触发
TypeError;不可变外观也不自动保证成员都可哈希。 - 把别名或浅复制误认为递归独立副本:
alias = original连外层也共享,浅复制只分离外层,嵌套的可变对象仍可能共享。 - 用
[[]] * count创建嵌套列表后假定每个内层独立:重复的是同一个内层引用,一处修改会出现在多个位置;应逐个创建确实独立的成员。 - 解包时忽略长度约束:没有星号目标时数量必须完全匹配;有一个星号目标也要为其他目标保留足够元素,否则抛出
ValueError。
本章只建立容器本身与复制边界,不讲列表、集合或字典推导式;该主题会在相应章节发布后单独链接。继续阅读运算符、表达式、条件、循环与模式匹配,学习容器如何参与表达式、分支与迭代控制。