编程语言研究书库

元组 — 只读的记录

元组 — 只读的记录

元组是列表的「只读变体」:同样的有序序列,同样的索引切片, 唯一的大区别——造好之后就不能改了。 听起来是缺陷,其实是设计: 有一批数据天生不该被改(地理坐标、数据库记录、函数的一次性返回), 元组的「不可变」就是你给这批数据上的锁。这一章很短,因为一半的 规则你已经会了。

1. 这一章在全书链条里的位置

读法建议:元组章是全书「复用率」最高的一章——索引、切片、遍历、 推导式四大件全部继承自前面两章,新知集中在「不可变」一个性质及其 四个推论(记录完整性、多值返回、字典键、打包解包)。带着 「这章主要是复习」的预期读,速度会快得惊人,也更清楚该在哪里 放慢——凡是说「和列表一样」的地方加速,凡是说「但」的地方减速。

第 11 章把列表立为「功能最全的基准」,本章做第一次减法:抽走「改」 这个抽屉。书的开头直接点明分工:列表可变,适合内容要动的场景; 元组不可变,适合固定数据、以及当字典的键(第 14 章会兑现)1。 第 09 章多值返回交还的那个带圆括号的输出,在这里正式领到身份证。

2. 顶层全景:一张「减去可变」的对照表

这张表是本节的全文浓缩,建议先扫一遍再往下读——带着「元组到底 少了什么」这个问题进正文,每一节的「不能改」都会变成一个有形的 缺口,而不是抽象的定义。

列表 list元组 tuple
写法[1, 2, 3](1, 2, 3)
有序、可混装、可索引、可切片✓✓(全继承)
增删改append/remove/sort…全部没有
改元素的后果成功TypeError
字典键资格✗(不可哈希)✓
典型场景一批会变的东西固定记录、函数返回、键

3. 核心原理一:为什么需要不可变 — 四个真实场景

一个快速类比:列表是白板,元组是打印纸。白板上写了能擦、 擦了能写,适合开会头脑风暴;打印纸一旦出来,内容就随它去 读者手里了——你敢保证每个人看到的都是同一版。程序里 「打印纸时刻」比你以为的多:一次函数返回、一份配置、 一组坐标——凡是「出了这个门就不该变」的数据,都该上打印纸。

书的开篇列了四类「天生不该改」的数据2:

  • 坐标:(43.6532, -79.3832)——多伦多的经纬度。改一个数, 就不是那个地点了;
  • 数据库记录:(John, Smith, 25, New York)——一行查询结果, 数据来自库,代码不该私改;
  • 商品信息:(Apple, iPhone, 128GB, $799)——一件商品的固定属性;
  • 时间:(2024, 3, 15, 14, 30, 0)——一个已经发生的历史时刻。

四例的共同点:数据有「身份」,改了就失真。元组的不可变是 语言层面的承诺——不是你「打算不改」,是根本改不了, 传给别人、存进别处都塌实。书里还有一句前瞻:元组「作为字典的键 更安全」1——字典键为什么要求不可变,第 14 章讲完字典就懂。

4. 核心原理二:创建 — 重点是那个逗号

创建一节只有一件事必须百分之百记住:单元素元组的逗号。 其余造法(list 同构)扫一眼就够。把这个逗号记成条件反射, 本章最大的一个坑就与你无关。

三种造法,前两种和列表同构3:

tuple()               # 空元组 → ()
tuple([1, 2, 3])      # 从列表转 → (1, 2, 3)
tuple('Hello, world') # 从字符串转 → 12 个字符的元组
(1, 2, 3)             # 圆括号直接写

然后是本章最著名的陷阱——单元素元组:

my_tuple_5 = (1,)   # 注意逗号!→ (1,)  这才是元组
test = (1)          # 没有逗号 → 1 <class 'int'>  这只是带括号的整数!

书里特意并排演示:(1,) 是元组,(1) 是 int4。为什么? 圆括号在 Python 里有两个兼职:表示元组、以及分组(数学里 先算括号里的)。(1) 被当成「分组」处理,包出来的还是 1 本身; 只有一个元素时,逗号才是「这是元组」的声明,圆括号反而可有可无。 推论:单元素元组 (1,) 的逗号不是排版癖,是语法必需。

len() 规则照旧:空元组 0,(1,) 是 1——那个元素算一个5。

主线例子:一条订单记录的一生

用书里练习区的那条订单记录走完元组的一生(每个操作注明出处):

order = ('Neo', 'iPhone', 999.99, 2, True)   # 创建:客户、商品、单价、数量、已付款
len(order)        # 5 ——异质五字段,一个元组装一条记录[^6]
order[0]          # 'Neo'   ——按位取,0 起[^7]
order[-1]         # True    ——负索引取末位[^7]
order[1:3]        # ('iPhone', 999.99)  ——切片,stop 不含[^8]
order[0] = 'Tom'  # TypeError ——不可变!这就是那条锁[^9]
  ① 创建  ('Neo', 'iPhone', 999.99, 2, True)
  ② 取    [0] → 'Neo';[-1] → True;[5] → IndexError(越界照旧报错)
  ③ 切    [1:3] → ('iPhone', 999.99);[::-1] → 整条倒序
  ④ 改    任何修改尝试 → TypeError,当场被拒

  图说:元组的一生没有「改」这个阶段——取与切照常,改直接撞墙。

5. 核心原理三:按位取、遍历、切片 — 全部继承

这三节有个共同的阅读姿势:每次书里写「similar to list」, 就在心里默一遍列表版的规则,然后只确认「输出括号变圆了」。 三轮下来,你等于把第 11 章又免费复习了一遍——这就是本书 「列表先行」编排的复利:每一章后容器,都让前容器更牢。

这三节是全书「复用度」最高的内容,规则与列表、字符串一字不差, 这里只点验差异678:

  • 索引:my_tuple_9 = ('apple', 'banana', 'orange'), [0] → apple,[-1] → orange,[3] → IndexError: tuple index out of range——报错信息只是把 list 换成 tuple6;
  • 遍历:for num in numbers: 直接迭代依旧是地道写法, 求和、计数(count_2、奇数计数)的累加器骨架原样复用7;
  • 切片:numbers = (0, 1, 2, …, 9) 的 [2:7]、[::2]、[::-1] 与列表输出逐项相同,只是括号从方变圆。正负混用五式 (letters = tuple('Python') 的 [2:-2] → ('t','h') 等) 也全部成立8。

值得停一秒的是这个观察:三处规则的「零成本迁移」正是当初 学字符串索引切片时说的「序列机制」——字符串、列表、元组三次应验, 第四次(第 13 章集合)将第一次「不应验」:集合无序,没有索引。 通过对比「谁继承、谁不继承」,你对「序列」这个词的理解会真正落地。

6. 核心原理四:元组推导式 — 其实是「生成器 + 元组()」

先说结论再拆机制:元组推导式的写法与列表推导式唯一的差别是 最外层由 [] 换成 tuple()——里面的表达式、for 从句、if 筛选, 一字不差。如果你第 11 章的推导式练熟了,本节可以三分钟通过。

书里的叫法是 tuple comprehension(元组推导式),语法一眼看上去 只是把方括号换成圆括号9:

squares_tuple = tuple(x ** 2 for x in range(1, 6))
# (1, 4, 9, 16, 25)
odd_numbers_tuple = tuple(x for x in [1, 2, 3, 4, 5] if x % 2 != 0)
# (1, 3, 5)
flattened = tuple(item for t in [(1, 2), (3, 4), (5, 6)] for item in t)
# (1, 2, 3, 4, 5, 6)
uppercase = tuple(word.upper() for word in ['apple', 'banana', 'cherry'])
# ('APPLE', 'BANANA', 'CHERRY')

一个技术细节值得说清(书里没讲,标为补充):(x for x in ...) 这种圆括号写法本身产出的是生成器(按需计算的惰性序列), 外面套的 tuple() 才把它物化成元组。所以「元组推导式」的准确读法是 「生成器表达式塞进 tuple()」。效果与列表推导一致,只是收口的 容器不同——记住「想要元组,推导完套 tuple()」即可。

练习的六道题把字符串谓词全用了一遍10:

students = ['Alice', 'Ann', 'Anny', 'Bob', 'Bobby', 'Bubbi']
line1 = tuple(s for s in students)                    # 全收
line2 = tuple(s for s in students if s[0] == 'A')     # 首字符 'A' → 三个 A 开头
line3 = tuple(s for s in students if s[-1] == 'y')    # 尾字符 'y' → ('Anny', 'Bobby')
line4 = tuple(s for s in students if len(s) < 4)      # 短名 → ('Ann', 'Bob')
pairs = tuple((a, b) for a in students for b in students)          # 36 对全组合
pairs = tuple((a, b) for a in students for b in students
              if a != b and a[0] == b[0])             # 同首字母且不同人

s[0] == 'A'、s[-1] == 'y'、len(s) < 4——第 05 章的字符串 索引和 len 在这里全部上岗;双 for + 双条件(a != b and a[0] == b[0]) 是第 11 章组合推导的元组版10。两章的知识在一个练习区会师。

7. 核心原理五:打包与解包 — 总结表里的隐形主角

这是全章「正文最少、分量最重」的一节:原书把打包/解包压缩在 总结表的最后三行,但它们其实是元组存在的最高理由——函数 多值返回、变量交换、zip 遍历,三个日常高频动作全靠它。 本节值得比书多花三倍时间。

原书总结表(Table 12.1)在最后三行放了本章真正的功夫,正文却没展开 ——打包(packing)与解包(unpacking)11:

# 打包:一串值不用括号也是元组
my_tuple = 1, 2, 3        # 等价于 (1, 2, 3)——逗号造元组,括号只是装饰

# 解包:一个元组按位置拆给多个变量
a, b, c = (1, 2, 3)       # a=1, b=2, c=3

解包的应用场景,前面章节已经见过三次,只是没点名:

  • 函数多值返回:return first, last, age, zip 是打包; a, b, c, d = get_profile() 是解包(第 09 章留白在这里补上);
  • 交换两个变量:a, b = b, a——右边打包、左边解包,一步到位;
  • zip 遍历:for name, age in zip(names, ages) 每次解包一个元组 (第 11 章推导式的 a, b in zip 同理)。

还有带星号的扩展解包(通用知识,不在书里):first, *rest = (1, 2, 3, 4) 让 first=1、rest=[2, 3, 4]——「拿走第一个,剩下全归 rest」。

交换变量:打包解包的最简实战

a, b = b, a 被称为 Python 的「一行交换」,很多新人以为它是特殊语法—— 不是。逐帧拆开看,全是本章旧知识11:

  第 1 帧  先算右边:b, a 打包成元组 (b_value, a_value)
  第 2 帧  再解包给左边:a 拿到 b_value,b 拿到 a_value
  第 3 帧  交换完成;全程没有中间变量,没有覆盖丢失

  图说:先整体打包、再整体解包——「同时」感来自这个顺序,
  不存在 a 先被覆盖的问题(C 语言交换需要第三个变量,Python 不需要)。

对比 C 风格的交换(temp = a; a = b; b = temp; 三行), 一行交换少一个临时变量、少一次「先覆盖后怕丢」的心智负担—— 这是打包/解包语法带来的红利,也是元组概念「活在日常代码里」的 最佳证据。

8. 元组与列表的完整选择判据

「什么时候用哪个」本章给了方向,补成可操作的判据表——写完一个 数据结构前过一遍:

判据问题选元组选列表
造好后还会增删吗?不会会
要当字典的键吗?(第 14 章)要不能
是函数的多值返回吗?默认数量不定才考虑
字段语义重要吗(记录)?字段少且固定字段多变 → 都该考虑字典
有别人会拿到它吗?怕它被别人改需要对方往里加

最后一行是元组最被低估的价值:接口安全。函数返回一个元组, 等于告诉调用方「这就是最终结果,别动」;返回列表,调用方可能 顺手 append,你的函数内部状态就漏了。把「只读意图」写进类型本身, 是元组对程序稳定性的真正贡献。

再补一条元组的隐藏福利(通用知识):元组可以无括号书写—— return a, b、x, y = y, x、for i, e in enumerate(...) 里的 成组逗号,全是打包/解包语法在发力。你早就在用元组了,只是没叫它名字。

9. 逗号陷阱的完整家族

(1) vs (1,) 不是孤例,逗号相关的坑还有三个(机制依据本章语法, 案例是我们的归纳)12:

  坑 1  (1)        → int 1       分组括号,不是元组
  坑 2  (1,)       → (1,)        单元素元组,逗号是本体
  坑 3  1, 2       → (1, 2)      无括号也是元组——打印时 Python 替你补括号
  坑 4  (1, 2) + (3,)  → (1, 2, 3)  元组可以拼接,结果还是元组

  图说:逗号造元组,括号只是盛装——单元素时盛装省略,逗号必须留下。

坑 3 的推论:交互式敲 1, 2 回显 (1, 2)——解释器把「打印形态」 的括号加上了,别以为自己敲了括号。坑 4 说明元组的「改」虽被封死, 「拼」依然合法:t = t[:2] + (新,) + t[3:](§9 的造新公式) 就是拼接加切片的组合。

10. 元组推导式与列表推导式的同与异

两者语法只有容器名之差,行为差异有两处值得点名9:

[x**2 for x in range(5)]          # 列表推导 → 直接得 [0, 1, 4, 9, 16]
tuple(x**2 for x in range(5))     # 元组版 → (0, 1, 4, 9, 16)

第一处:列表推导本身就是一个表达式,输出列表;元组版是 「生成器表达式 + tuple()」两层——想写 (... for ...) 直接当 推导式用也行,但拿到的是生成器,得套 list/tuple 才能看内容。 第二处:推导式的产出容器决定结果的「性格」——同一行逻辑, 套 list 得可变的,套 tuple 得只读的。推导式不挑容器,挑容器的是 你对结果可变性的要求。

练习里 pairs 那题的自组合输出值得多看一眼:(a, b) for a in students for b in students 产出 36 对(6×6),换成带双条件的版本后只剩 10 对 (同首字母、排除自身)——条件的威力在组合问题上成倍放大, 这正是双 for 推导的主场10。

11. 不可变的真正含义:三个层次

「不可变」三个字,层层剥开有三层意思,一层比一层深13:

  第 1 层  不能换元素    t[0] = 5        → TypeError
  第 2 层  不能增删      t.append(5)     → AttributeError(元组根本没这方法)
  第 3 层  元素自己能变  t[1].append(5)  → 合法!锁不锁内层

  图说:前两层是「元组的锁」,第三层是「元素自己的门」——
  元组只保证名单不变,名单上的人回家干什么,它不管。

第 3 层的例子再敲一遍:t = (1, [2, 3]); t[1].append(4) 完全 合法,t 变成 (1, [2, 3, 4])——元组的 len 没变、「名单」没变, 但名单里的列表内容变了。这就是 §12 边界节说的「浅层不可变」, 它和第 11 章的浅拷贝是同一个家族:Python 的容器守卫只站一层岗。 想连内层一起冻住?装不可变内容(数字、字符串、元组)即可—— (1, (2, 3)) 是真的全冻住了。

12. 哈希与字典键:元组的资格证明

书里两次提到「元组能当字典键」1,这里把机制说透(第 14 章的 预备知识):字典靠哈希(hash,把对象算成一个固定整数的函数) 来快速找键。哈希有一个硬要求:对象一辈子算出来的整数不能变—— 否则存进去时算的数和查找时算的数对不上,字典就瞎了。 可变对象(列表)的哈希会变,所以 Python 干脆不允许它当键; 不可变对象(数字、字符串、元组)哈希稳定,准予上岗。

  键资格检查的两道门:
    门 1  它不可变吗?        元组 ✓ 列表 ✗
    门 2  它的元素也不变吗?   (1, [2]) ✗ 元素有列表,拒收
                                  (1, (2,)) ✓ 全员不可变,放行

  图说:第二道门就是 §11 的第三层——浅层不可变,浅层资格审查。

把这个机制记住,第 14 章讲「列表不能当键」时就不用死记了: 不是 Python 刁难,是哈希的数学要求,绕不过去14。

13. 函数多值返回:第 09 章承诺的兑现

第 09 章的 return first, last, age, zip 在这里正式揭开机制—— 那个 return 打包了一个元组,调用方的 a, b, c, d = get_profile() 解包收四件。把往返全程画出来11:

def get_profile():
  first = input('first: ')     # Thomas
  last = input('last: ')       # Anderson
  age = int(input('age: '))    # 18
  zip_code = input('zip: ')    # 09090
  return first, last, age, zip_code   # 打包:(打包成四元组)

result = get_profile()         # result 是 ('Thomas', 'Anderson', 18, '09090')
f, l, a, z = result            # 解包:四个变量各领一件
  去程:四个值 → 逗号打包 → 一个元组 → 交还给调用处
  回程:调用处 → 解包给多个变量(或整体存进一个变量)
  全程没有魔法,就是 §7 的打包/解包在函数边界上各做一次。

  图说:return 多个值 = 打包;多个变量接收 = 解包。
  交换 a, b = b, a 是同一机制的最简形态(打包(b,a)再解包)。

为什么函数多值返回默认该用元组而不是列表?三条理由:数量与身份固定 (四个字段不会中途加第五个);调用方不该往里 append(只读意图); 不可哈希无所谓但不可变传递更安全。第 09 章 stats 返回 (1, 3, 2.0, 6) 用的就是元组——当时只认得「圆括号」,现在 认得它的全部含义。

14. 遍历与切片练习走查

两节练习各有代表题,快速走一遍78:

遍历四题(numbers = (1, 2, 2, 3, 5)):逐元素打印、求和 (累加器得 13)、数 2 的个数(计数器得 2)、数奇数(i % 2 != 0 当筛,得 3)。四题的骨架与第 11 章列表版逐字同构——再次实证 「机制跨容器复用」:换的只是容器类型,循环本身一行不改。

切片六题(students = ('Alice', 'Bobby', 'Cathy', 'David', 'Ethan', 'Frank')):

  students[:3]      → ('Alice', 'Bobby', 'Cathy')   前三
  students[-3:]     → ('David', 'Ethan', 'Frank')   末三(负起点+省终点)
  students[2:-2]    → ('Cathy', 'David')            掐头去尾各二
  students[::2]     → ('Alice', 'Cathy', 'Ethan')   隔一采样(分队 1)
  students[::3]     → ('Alice', 'David')            隔三采样(分队 2)
  students[::-1]    → 整列倒序                      Frank 打头

  图说:六刀全部命中,且 team1/team2 的命名揭示了切片的一个真用途——
  按固定间隔「分队」,原序列不动,新元组上岗。

「分队」这个练习设计很妙:::2 和 ::3 把六人按不同间隔分成两组, 输出恰好互补(Cathy、Bobby、Ethan 未进队)——切片不是只用来 「取一段」,还是「按模式抽样」的工具。

15. 解包的完整语法家族

打包/解包不止 a, b, c = t 一种形态,把全家族摆开(机制是通用知识, 部分超出原书范围)15:

# 基本解包:数量必须匹配
a, b, c = (1, 2, 3)

# 星号解包:一人吃剩余
first, *rest = (1, 2, 3, 4)      # first=1, rest=[2, 3, 4](剩余归列表)
*a, last = (1, 2, 3, 4)          # a=[1, 2, 3], last=4

# 丢弃不想要的:约定用下划线
name, _, price = ('Apple', 'iPhone', 799)

# 交换:打包+解包的合体
a, b = b, a

# 嵌套解包:结构对结构
(a, (b, c)) = (1, (2, 3))        # a=1, b=2, c=3

星号解包是家族里最实用的扩展:「拿走第一个,剩下全归我」在 数据处理里天天发生(取表头、取队首)。下划线丢弃则是社区约定—— Python 不会拦你使用 _,但读到 _ 的人都知道「这位置有意不要」。 数量不匹配时,基本解包报 ValueError: too many values to unpack—— 报错信息本身就是解释:值太多(或太少),解不了。

16. 元组方法速查与「想改怎么办」决策树

元组的方法只有两个,配查询场景一张表11:

方法干什么备注
t.count(x)数 x 出现几次与列表版同
t.index(x)x 第一次出现的位置找不到报 ValueError

「想改元组怎么办」的决策树(承接 GenAI 区之问)16:

  想改的是单个元素?
    └─ 是的 → 造新:t2 = t[:i] + (新值,) + t[i+1:]
  想加/删元素?
    └─ 是的 → 造新:t2 = t + (新元素,) / t[:i] + t[i+1:]
  这样的「改」很频繁?
    └─ 是的 → 说明当初选错了容器,改用列表(或字典)

  图说:前两条是「切片加拼接」的变体;第三条是最重要的一条——
  频繁的「造新」是元组在抗议,它在告诉你该换容器了。

17. 创建练习走查:四种原料四种造法

12.2.2 的四道创建练习,把元组的四种「原料」各走一遍35:

  tuple('Tuple Constructor')  从字符串 → 17 个字符(逐字符拆,含空格)
  tuple([5, 7])               从列表   → (5, 7)(序列间互转,数据不变)
  tuple([5.5, 5.2, 5.0])      从列表   → 三个浮点
  ('Apple', 'Pear')           直接书写 → 最常用

  图说:tuple() 是「任何序列转元组」的万能转换器——和 list() 同族,
  方向相反。学完四种容器你会发现这对转换器有四个:list/tuple/set/dict。

注意第二行:tuple([5, 7]) 从列表转元组是数据搬家——内容照抄, 「可变性」这层壳换掉。反向 list((5, 7)) 同样合法。容器之间的 互相转换是数据处理的家常操作:选对容器干活,转对了再传。

18. 三容器性格总表(字符串/列表/元组)

到这里,三种「序列」全部登场,把第 11 章 §27 的对照表升级成三列版—— 这张表是前三章知识的总收口:

维度字符串元组列表
内容只字符任意任意
可变性不可变不可变可变
写法'...'(1, 2)[1, 2]
拼接+ 造新+ 造新+ 造新 / += 原地
反转[::-1][::-1][::-1] 或 reverse()
方法数四十余(第 05 章表)两个(count/index)二十余
当字典键✓✓(元素不可变时)✗
典型角色文本记录、返回、键主力容器

从上表读出一个递进:方法数量与可变性正相关——能改的东西才 需要一大家子修改方法,不能改的只需要查询员。元组那两个方法 (count/index)是「只读查询」的最小集,字符串的四十几个方法里 大半也是只读查询(upper/find/split),变化的只是文本处理的需求多。

19. 推导式练习的谓词解剖

12.7.2 的四道筛选题(students = ['Alice', 'Ann', 'Anny', 'Bob', 'Bobby', 'Bubbi']), 四个谓词各用了一种「字符串体检」手段,值得逐个解剖10:

line2 = tuple(s for s in students if s[0] == 'A')   # 首字符检查
line3 = tuple(s for s in students if s[-1] == 'y')  # 尾字符检查
line4 = tuple(s for s in students if len(s) < 4)    # 长度检查
pairs = tuple((a, b) for a in students for b in students
              if a != b and a[0] == b[0])           # 双变量交叉条件
  输出对照:
  line2 → ('Alice', 'Ann', 'Anny')     首字符 A,Bob 家族全淘汰
  line3 → ('Anny', 'Bobby')            尾字符 y;Alice/Ann 以 e/n 收尾落选
  line4 → ('Ann', 'Bob')               长度 < 4;Alice(5 字符)落选
  pairs → 10 对同首字母的互异配对

  图说:s[0] / s[-1] / len(s) 是字符串体检三件套——首、尾、长,
  分别对应「属于哪一组」「怎么结尾」「什么体量」。

第四题的 a[0] == b[0] 是一个新句式:同一个谓词比较两个变量 ——不再是「元素 vs 常量」,而是「元素 vs 元素」。这个句式是 配对问题的灵魂:同组配对、找相似、查重复,全靠它。注意条件里 a != b 写在前面:先排除自己配自己,再看首字母——两个条件 用 and 串联,缺一不可。

20. 作者的判断与证据

判断(我们的,不是书里的): 元组章在四种容器章节里篇幅最短 (原书 15 页,列表 24 页),这不是元组不重要,而是它的故事 「一半在别处」——索引切片在字符串章讲过,打包解包在函数章 用过,键资格在字典章才兑现。本章真正的原创内容约占三分之一: 不可变的意义、逗号陷阱、选择判据。读时把预期调成 「三分之一新知 + 三分之二复用」,体验会好很多。 如果错,会错在: 若未来版本的原书扩充了 namedtuple 或 数据类(dataclass)的内容,本章的「内容占比」判断就会失效—— 那是元组故事的新章节,值得期待而不是担忧。

  • 「元组 = 只读列表,用于固定数据」的定位与 Python 官方文档的口吻 一致;书里「更轻量、数据完整性、性能更好」的三个优点1, 前两个是实义,第三个(性能)对入门读者感受不明显——元组确实 略省内存、略快,但这不是选它的主要理由,别被带偏。
  • 「单元素要逗号」的陷阱安排在创建一节的最显眼处,还配了 (1) vs (1,) 的对照输出——这是本章最有教学价值的一屏4。
  • 总结表比正文宽(packing/unpacking 只在表里),第 11 章的老编排 再次出现;我们 §7 已补11。
  • 全章对「什么时候用元组、什么时候用列表」给了场景答案 (固定数据 → 元组),但没给「记录型数据 vs 字典」的判据—— 订单记录这种「有字段语义」的数据,工程上多数时候选字典, 元组方案适合字段少且固定的场合(两个补充,见边界节)。

21. 边界与局限

判断(我们的,不是书里的): 教元组最容易犯的错误是把它讲成 「更好的列表」或「受限的列表」——两个说法都把不可变当成了缺陷。 本章的四个场景(坐标/记录/商品/时间)提供了正确的框架: 不可变不是限制,是承诺;选元组不是接受降级,是给出保证。 带着「承诺」的心智模型出门,后面读到多线程、函数式、字典键 时,每一处都会有一声「原来如此」。 如果错,会错在: 「承诺」框架对临时草稿数据过强—— exploratory 阶段的一切容器都该用列表,直到形状稳定再冻结成元组; 「先列表后元组」的演化路径,比「一开始就当机立断」更符合 真实开发。

判断(我们的,不是书里的): 元组章的正确读法是「一半学元组, 一半复习列表」。凡是与列表相同的规则(索引、切片、遍历、推导), 读速可以放快——真正的新知只有四件:不可变的意义、逗号陷阱、 打包/解包、键资格。把这四件吃透,本章就算超额完成。 如果错,会错在: 若读者对列表章还不熟,本章的「快速复读」会 变成「夹生复习」——两章都似懂非懂。先回去把第 11 章的切片十二式 练熟,再进本章,是更稳的顺序。

先置顶本章最常被问的三问:① 单元素元组为什么要逗号?(§4—— 逗号是元组的声明,括号会被当成数学分组)② 元组怎么「改」? (§16——造新;造得勤说明容器选错了)③ 元组能当字典键、列表为什么不能? (§12——哈希要求一辈子不变的整数,可变对象做不到)。三问之外:

  • 「不可变」是浅层的:元组里的列表照样能改。 (1, [2, 3]) 不能换那个列表,但 t[1].append(4) 完全合法——锁只锁第一层。 这和第 11 章的浅拷贝是同一族问题:不可变容器装可变内容, 内容不享有保护。
  • 元组几乎没有方法。 没有 append/remove/sort 全家桶, 只有 index()(找位置)和 count()(数次数)两个查询员, 外加 count/index 在总结表里。想「改」元组的唯一正路是 造新的:t = t[:2] + (新元素,) + t[3:]——切片加拼接, 和第 05 章改字符串同一个套路。GenAI 区问的「如何绕过不可变」, 答案就是这句16。
  • 元组当字典键的资格有个前提:元素都得不可变。 (1, 2) 能当键,([1], 2) 不能——元组的不可变是浅的,键的哈希要求也是 浅的检查到底层。第 14 章用到时再细说。
  • 记录型数据的标准容器是字典,不是元组。 订单记录用元组装, 取字段靠位置(order[2] 是单价?忘了),字段一多就崩; 字典按键取名(order['price']),自解释。元组留给:坐标等 位置本身有语义的成组数据、函数返回、以及字典键。
  • 没有讲具名元组(namedtuple)。 标准库 collections 里的 namedtuple 让元组字段有了名字(兼顾不可变和可读性)—— 它是「元组 vs 字典」之争的优雅和解,本书没提,但值得知道存在 (第 16 章的三件套之一就是它的现代版,见后文)。
  • 元组的性能优势被一笔带过。 书里只说「更轻量」——具体说, 元组内存略省、创建略快,但这些差异在入门规模下完全感知不到; 选元组的理由永远是语义(不可变),不是性能。谁拿性能当 第一理由,让他先拿出 profile 数据。

22. 可带走的

一分钟自查:能否不看资料写出——单元素元组、元组转列表、 解包交换两个变量、「尾字符是 y 的筛选」推导式?四问全对, 本章毕业。

  1. 元组 = 不可变列表:有序、异质、可索引、可切片;没有增删改; 圆括号是装饰,逗号才是元组的声明1。
  2. 单元素元组必须带逗号:(1,) 是元组,(1) 是 int4。
  3. 索引越界照旧 IndexError,报错文案只换对象名6。
  4. 遍历与切片的全部规则继承,切片越界宽容依旧87。
  5. 「元组推导式」= 生成器表达式套 tuple()9。
  6. 打包 1, 2, 3 与解包 a, b, c = t 是函数多值返回与变量交换的底层机制11。
  7. 不可变是浅层的:元组里的列表能改;当字典键要求元素全不可变。
  8. 想改元组?切片加拼接造新的——和改字符串同款套路; 造得勤就说明容器选错了,该换列表16。
  9. 元组的方法只有 index/count;记录型数据多数时候投奔字典。

附「四问自查」的答案核对:单元素元组是 (x,)(逗号必须); 元组转列表用 list(t);交换是 a, b = b, a;筛选是 tuple(s for s in xs if s[-1] == 'y')。

23. 原文地图

使用说明:短语逐字取自原书转码文本,可用 node scripts/book-verify.mjs bitesize-python-beginners 逐条回核; 行号若因重新转码漂移,以短语搜索为准。

主题原书章原文位置
元组定位(可变/不可变分工、字典键)CHAPTER 12: Tupletext/21-ch12-chapter-12-tuple.txt:4(搜「Lists and tuples are both」) · text/21-ch12-chapter-12-tuple.txt:8(搜「as keys in dictionaries」)
四个真实场景(坐标/记录/商品/时间)同上text/21-ch12-chapter-12-tuple.txt:19(搜「43.6532」)
三种创建与逗号陷阱同上text/21-ch12-chapter-12-tuple.txt:38(搜「Create an empty tuple」) · text/21-ch12-chapter-12-tuple.txt:60(搜「Note the comma」) · text/21-ch12-chapter-12-tuple.txt:65(搜「test = (1)」)
len 规则同上text/21-ch12-chapter-12-tuple.txt:80(搜「length or the number」)
异质元组与嵌套同上text/21-ch12-chapter-12-tuple.txt:161(搜「HETEROGENEOUS TUPLE IN PYTHON」) · text/21-ch12-chapter-12-tuple.txt:180(搜「it is still counted as 1」)
索引与负索引、越界同上text/21-ch12-chapter-12-tuple.txt:214(搜「accessing elements in a tuple」) · text/21-ch12-chapter-12-tuple.txt:246(搜「tuple index out of range」)
遍历两种方式同上text/21-ch12-chapter-12-tuple.txt:331(搜「sequential processing of each element」) · text/21-ch12-chapter-12-tuple.txt:342(搜「for i in range(len(numbers))」)
切片全式同上text/21-ch12-chapter-12-tuple.txt:387(搜「extraction of a subset」) · text/21-ch12-chapter-12-tuple.txt:396(搜「numbers = (0, 1, 2, 3」) · text/21-ch12-chapter-12-tuple.txt:440(搜「letters = tuple('Python')」)
切片练习六式同上text/21-ch12-chapter-12-tuple.txt:472(搜「students = ('Alice', 'Bobby'」)
元组推导式同上text/21-ch12-chapter-12-tuple.txt:510(搜「12.7 TUPLE COMPREHENSION」) · text/21-ch12-chapter-12-tuple.txt:521(搜「tuple(x ** 2 for x」)
推导式练习(谓词与配对)同上text/21-ch12-chapter-12-tuple.txt:552(搜「line1 = tuple(s for s」) · text/21-ch12-chapter-12-tuple.txt:576(搜「pairs = tuple((a, b)」) · text/21-ch12-chapter-12-tuple.txt:593(搜「a != b and a[0] == b[0]」)
总结表(打包/解包/index/count)同上text/21-ch12-chapter-12-tuple.txt:606(搜「Summary of Python tuples」) · text/21-ch12-chapter-12-tuple.txt:616(搜「Packing」) · text/21-ch12-chapter-12-tuple.txt:622(搜「tuple.index(item)」)
GenAI 互动区(绕过不可变)同上text/21-ch12-chapter-12-tuple.txt:649(搜「work around tuple immutability」)

Footnotes

  1. 出处:「CHAPTER 12: Tuple」第 3–9 段(text/21-ch12-chapter-12-tuple.txt:4,搜「Lists and tuples are both」;:8,搜「as keys in dictionaries」;:8`,搜「especially useful when you need lightweight」)。可变/不可变的分工与三优点逐字依据原文。 ↩ ↩2 ↩3 ↩4 ↩5

  2. 出处:「CHAPTER 12: Tuple」第 15–30 段(text/21-ch12-chapter-12-tuple.txt:19,搜「43.6532」;:21,搜「John」;:26,搜「2024, 3, 15」)。四个场景的类型与数值逐字依据原文。 ↩

  3. 出处:「CHAPTER 12: Tuple」第 33–78 段(text/21-ch12-chapter-12-tuple.txt:38,搜「Create an empty tuple」;:43,搜「tuple([1, 2, 3])」;:48,搜「tuple('Hello, world')」;:55,搜「my_tuple_4 = ()」;:70,搜「my_tuple_6 = (1, 2, 3)」)。四种创建与输出逐字依据原文。 ↩ ↩2

  4. 出处:「CHAPTER 12: Tuple」第 60–68 段(text/21-ch12-chapter-12-tuple.txt:60,搜「Note the comma」;:65,搜「test = (1)」;:68,搜「<class 'int'>」)。(1,) 与 (1) 的对照输出逐字依据原文;「逗号是声明」的机制解读是我们的表述。 ↩ ↩2 ↩3

  5. 出处:「CHAPTER 12: Tuple」第 80–113 段(text/21-ch12-chapter-12-tuple.txt:86,搜「output 0」;:106,搜「output 1」)。len 的五个输出(0/3/12/0/1/3)逐字依据原文。 ↩ ↩2

  6. 出处:「CHAPTER 12: Tuple」第 211–326 段(text/21-ch12-chapter-12-tuple.txt:223,搜「my_tuple_9 = ('apple', 'banana', 'orange')」;:246,搜「tuple index out of range」;:261,搜「my_tuple_9[-1]」;:289,搜「tuple_practice = (2, 4, 6, 8, 10)」)。索引八连问与两类越界报错逐字依据原文。 ↩ ↩2 ↩3

  7. 出处:「CHAPTER 12: Tuple」第 328–383 段(text/21-ch12-chapter-12-tuple.txt:339,搜「numbers = (1, 2, 3, 4, 5)」;:364,搜「Calculate the sum in numbers」;:370,搜「Count how many 2」;:377,搜「number of odd integers」)。两种迭代与求和/计数练习见原文。 ↩ ↩2 ↩3 ↩4

  8. 出处:「CHAPTER 12: Tuple」第 384–508 段(text/21-ch12-chapter-12-tuple.txt:396,搜「numbers = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9)」;:407,搜「numbers[3:11]」;:433,搜「numbers[::-1]」;:440,搜「letters = tuple('Python')」;:446,搜「letters[2:-2]」;:466,搜「letters[:1:-1]」)。切片全式与六连问输出逐字依据原文。 ↩ ↩2 ↩3 ↩4

  9. 出处:「CHAPTER 12: Tuple」第 510–545 段(text/21-ch12-chapter-12-tuple.txt:513,搜「similar to list comprehension」;:518,搜「tuple(expression for item in iterable」;:521,搜「tuple(x ** 2 for x in range(1, 6))」;:535,搜「flattened_tuple」)。四个推导式例及输出逐字依据原文;「生成器 + tuple()」的机制是通用知识补充。 ↩ ↩2 ↩3

  10. 出处:「CHAPTER 12: Tuple」第 547–602 段(text/21-ch12-chapter-12-tuple.txt:552,搜「line1 = tuple(s for s in students)」;:558,搜「s[0] == 'A'」;:564,搜「s[-1] == 'y'」;:570,搜「len(s) < 4」;:576,搜「pairs = tuple((a, b)」;:593,搜「a != b and a[0] == b[0]」)。六道练习的输出(('Alice','Ann','Anny')、('Anny','Bobby') 等)逐字依据原文。 ↩ ↩2 ↩3 ↩4

  11. 出处:「CHAPTER 12: Tuple」第 604–624 段(text/21-ch12-chapter-12-tuple.txt:606,搜「Summary of Python tuples」;:616,搜「Packing」;:619,搜「Unpacking」;:622,搜「tuple.index(item)」;:623,搜「tuple.count(item)」)。打包/解包/index/count 四行只在总结表出现——我们的 §7 扩写即补此留白。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6

  12. 补充(不在书里,来自通用知识):四个逗号坑的机制(分组 vs 元组、无括号打包、拼接合法性)依据 Python 语法定义;「逗号造元组、括号只是盛装」的口诀是我们的提炼。 ↩

  13. 出处:「CHAPTER 12: Tuple」第 604–615 段(text/21-ch12-chapter-12-tuple.txt:614,搜「my_tuple[0] = 5 raises TypeError」)。不可变的 TypeError 证据出自总结表。 ↩

  14. 补充(不在书里,来自通用知识):哈希稳定性与「可变对象不可哈希」的机制依据 Python 官方文档对 hashable 的定义;两道门的比喻是我们的讲解方式。 ↩

  15. 补充(不在书里,来自通用知识):解包全家族的语法(星号、下划线、嵌套)依据 Python 官方文档;「频繁造新就该换容器」的决策判据是我们的经验建议。 ↩

  16. 出处:「CHAPTER 12: Tuple」第 648–650 段(text/21-ch12-chapter-12-tuple.txt:649,搜「work around tuple immutability」)。「绕过不可变」之问出自 GenAI 互动区,正文未答;「切片加拼接造新」的答案是通用做法。 ↩ ↩2 ↩3

On this page