跳转至

命名元组 — 给元组的每个位置起名字

这一章讲一件事: 元组存记录很方便,但 record[2] 这种写法看不出第 2 位 存的到底是什么。namedtuple(命名元组)给每个位置贴上名字,让 record[2] 变成 record.status——读代码的人不用回头查定义,一看名字就懂。 这是第四篇的开篇,也是「标准库替你造好的轮子」的第一课。

1. 这一章讲什么、在全书链条里的位置

第 11–14 章学的四种结构都是 Python 的内置类型,开机就有。 第四篇(Data Collections)换了一个角度:标准库里有一个 collections 模块, 里面装着几种为常见需求特制好的加强版结构——这一篇的三章,就是它最常用的三件: 命名元组(本章)、默认字典(下一章)、计数器(再下一章)1。

本章主角 namedtuple 解决的具体问题是:元组按位置取数的可读性代价。 机制上没有新东西——它就是一个元组,只是每个位置多挂了一个名字。

2. 顶层全景:一行定义,两种取法

  from collections import namedtuple
  Position = namedtuple('Position', ['x', 'y'])   ← 定义:造一个「带名字的元组类型」
        │
        ├─ position1 = Position(x=10, y=20)       ← 造实例:像函数调用一样填值
        │
        ├─ position1.x  → 10    ← 按名字取(新本领)
        └─ position1[0] → 10    ← 按位置取(老本领,照常用)

  图说:实例里存的还是一列数,只是每个位置登记了一个名字;两种取法指向同一个值。

主线例子用平面上的点:两个数据点,坐标分别是 (10, 20) 和 (-5, 30)2。 下面每个机制都走这组数。

3. 核心原理:两个机制,各走一步

3.1 没有名字之前:位置号是最好的情况了

先看要解决的问题。普通元组表示一个点:

  position1_tuple = (10, 20)

要取 x 坐标,只能写 position1_tuple[0];取 y,写 position1_tuple[1]。 两个位置还记得住,可是一条记录有五个、十个字段呢? 原书的评价是:靠位置号「不够直观,在更大的程序里还可能引起混淆」3—— 代码写到一半,你很难断定 [0] 到底是 x 还是 y,得翻回定义处重看。

3.2 有了名字之后:定义一次,两处都能取

命名元组的造法分两步,语法和普通元组完全不同,值得逐行看清:

第一步,从 collections 模块把 namedtuple 请进来,并「定义一个新类型」:

from collections import namedtuple
Position = namedtuple('Position', ['x', 'y'])

namedtuple('Position', ['x', 'y']) 的意思是: 造一个名叫 Position 的元组类型,它有两个位置,第一位登记的名字叫 x,第二位叫 y。 这一行运行完之后,Position 就可以像内置类型一样用了。 (collections 是 Python 标准库里的一个模块——一批已经写好的工具的集合; import 是把工具请进当前程序的动作。)

第二步,像调函数一样造出实例,填进具体的数:

position1 = Position(x=10, y=20)
position2 = Position(x=-5, y=30)

然后两种取法同时成立:

position1.x   # → 10   按名字取
position1[0]  # → 10   按位置取,和普通元组一模一样

主线例子的运行结果:x=10、y=20;x=-5、y=30——两个点各自的两样数, 用名字和用位置号取出来完全相同2。

为什么有效: 名字是登记在「类型」那一层的,所有实例共享一份登记册。 所以取名零额外成本,实例里存的还是一列数——这正是它「轻量」的原因4。

顺带把 namedtuple 这个词本身的结构拆开:named + tuple,「被命名了的元组」; 它在标准库里的角色是个工厂函数——你调它,它不直接给你数据, 而是给你一台「造带名字元组的机器」(也就是 Position 这个新类型)。 理解这一层, collections 三件套的后两件就顺了:defaultdict 和 Counter 同样是 「你给个规格,它给你一台顺手的机器」。

3.3 名字不妨碍老本领

原书特意用快递单的例子演示:命名元组两种取法并存5。 一个包裹有三个字段——sender(寄件人)、receiver(收件人)、status(状态):

Package = namedtuple('Package', ['sender', 'receiver', 'status'])
package1 = Package(sender='Alice', receiver='Bob', status='Delivered')
package2 = Package(sender='Eve', receiver='Chris', status='In Transit')

取数时,想读得懂就按名字:package1.sender → 'Alice'; 想按老习惯就按位置:package1[0] → 还是 'Alice'。 两种方式指向同一个值,原书把两行写法并排印了出来5。

3.4 汽车档案:一次把「定义—造实例—读字段」走全

章末案例把日常用法压缩成一个完整小循环:给汽车定四个字段—— make(厂商)、model(车型)、year(年份)、color(颜色),然后造两辆车9:

Car = namedtuple('Car', ['make', 'model', 'year', 'color'])
car1 = Car(make='Toyota', model='Corolla', year=2020, color='Blue')
car2 = Car(make='Honda', model='Civic', year=2019, color='Red')

打印时按名字读:car1.make → 'Toyota',car1.year → 2020; 两辆车并排,一行一个,各字段一目了然。 这个例子没有新机制——它的意义在于示范了字段一多,名字的价值就翻倍: 四个字段靠位置号(0、1、2、3)记起来已经开始吃力,而 car2.color 不需要任何记忆。

顺带一提另一个没展开的老本领:命名元组仍是元组,所以第 12 章的 打包/解包(pair unpacking)对它照样有效——一行 make, model, year, color = car1 能把四个字段同时拆进四个变量。 原书正文没有演示,只把「怎么解包」留在了给 AI 的提问清单里10。

4. 作者的判断与证据

作者明说的: namedtuple「在不给完整类(class)添负担的情况下表示简单的数据 结构」6——这是原书给它的定位。这里的「类」是面向对象编程里的概念, 你暂时只需知道:那是一个更重、更正式的打包数据的工具,而 namedtuple 是轻量替身。

证据: 本章的全部论据就是三组可运行的例子——坐标点(3.2 节)、快递单(3.3 节)、 以及章末案例里的汽车档案(下一节)。每个例子都演示了「按名字取 + 按位置取」并存, 没有性能测量,没有和字典的对照实验。这是个「看用法就明白值不值」的工具, 原书选择了直接给你看用法。

我们的解读(不是书里的): 这个工具的价值不在新能力,而在读代码时的认知负担。 package1.status == 'Delivered' 和 package1[2] == 'Delivered' 做的事一模一样, 但前者一眼就懂,后者要你记住第 2 位是状态。代码被读的次数远多于被写的次数, 省的是读的那一边。

判断(我们的,不是书里的): namedtuple 是「记录类数据」的默认起点—— 比字典省内存、比类省笔墨,还自带两种取法。真正的取舍只在它和 dict 之间: 字段焊死、只读、形状固定选 namedtuple;要增删字段、或键本身动态变化选 dict。 如果错,会错在: 如果程序后来要求「给记录加一个新字段」,namedtuple 的 字段表焊死了,改起来比 dict 麻烦——选型时对未来变化的预期若错了,这份「轻量」 就会变成返工成本。

5. 边界与局限

名字只在定义时给一次,之后不能加字段。 命名元组继承元组的只读特性: 实例造好之后,既不能改字段的值,也不能往里添字段。 想「改」,唯一的办法是造一个新实例(标准库提供 _replace() 干这件事, 原书没有展开,只在给 AI 的提问清单里点了一句)7。

它只适合「记录形状固定」的数据。 字段表在定义那一行就焊死了: 每个 Position 都必须恰好有 x 和 y。如果你的数据行长短不一、字段时有增减, 该回头用字典(第 14 章)。两条判据的走查:字段表焊死、取值多于改值, 选 namedtuple——坐标点、档案、单据都是这一类; 字段要增删、或键本身就是数据里读出来的(今天三个字段明天四个), 选 dict——配置表、翻译表都是这一类。选错的代价不对称:从 namedtuple 改去 dict 是重写定义一行,从 dict 改回 namedtuple 要动所有增删字段的代码, 所以拿不准时先 dict,记录形状真稳定了再收紧。

原书把 namedtuple 的意义压在「可读性」三个字上:给每个元素起有意义的名字, 代码更清楚、更好维护,大项目里出错的风险跟着降11。

原书没讲的: 和字典互为替代时怎么选(字典能增删字段,命名元组更快更省); _fields、_asdict() 这两个配套小工具也只出现在 AI 提问清单里,正文没教7。 这些是查阅官方文档能补上的,原书把链接放在了章末8。

6. 可带走的

  1. 元组按位置取数记不住、分不清时,就该想 namedtuple 了;
  2. 用法两步:命名 = namedtuple('命名', ['字段1', '字段2']),然后像函数一样 实例 = 命名(字段1=值, 字段2=值);
  3. 命名元组按名字取、按位置取都行,老代码的取法不用改;
  4. 它仍是元组:造好就不能改、不能加字段;要「改」只能造新实例;
  5. 字段表在定义时焊死,适合形状固定的记录(坐标、档案、单据),不适合字段乱变的;
  6. 它住在标准库的 collections 模块里——from collections import namedtuple 一行请进来;
  7. 什么时候不用它:记录要增删字段时用 dict;只需要两三个位置且一眼能看懂时,普通元组就够了。

7. 原文地图

主题 原书章 原文位置
第四篇三件套的定位 SECTION IV: Data Collections text/25-fm-section-iv-data-collections.txt:2(搜「collections module」)
namedtuple 是什么(解释) SECTION IV: Data Collections text/25-fm-section-iv-data-collections.txt:29(搜「extension of Python's built-in tuple」)
普通元组取数的别扭 SECTION IV: Data Collections text/25-fm-section-iv-data-collections.txt:41(搜「regular tuple」)
Position 定义与两种取法 SECTION IV: Data Collections text/25-fm-section-iv-data-collections.txt:55(搜「from collections import namedtuple」) · :65(搜「position1.x」)
快递单案例(两种取法并存) SECTION IV: Data Collections text/25-fm-section-iv-data-collections.txt:76(搜「logistics company」) · :93(搜「Accessing data by index」)
「不给完整类添负担」的定位 SECTION IV: Data Collections text/25-fm-section-iv-data-collections.txt:34(搜「without the overhead」)
汽车档案案例 SECTION IV: Data Collections text/25-fm-section-iv-data-collections.txt:107(搜「CASE STUDY: CAR」)
_replace 等进阶工具(仅提问清单) SECTION IV: Data Collections text/25-fm-section-iv-data-collections.txt:136(搜「_replace」)
官方文档链接 SECTION IV: Data Collections text/25-fm-section-iv-data-collections.txt:144(搜「EXPLORE MORE」)
篇首的可读性论点 SECTION IV: Data Collections text/25-fm-section-iv-data-collections.txt:22(搜「meaningful names」)

  1. 出处:「SECTION IV: Data Collections」第 2–15 段(text/25-fm-section-iv-data-collections.txt:2,搜「collections module」)。篇首介绍:explore three essential tools from the collections module——namedtuple、defaultdict、Counter。 ↩

  2. 出处:「SECTION IV: Data Collections」第 38–72 段(text/25-fm-section-iv-data-collections.txt:41,搜「regular tuple」;:55,搜「from collections import namedtuple」;:58,搜「'Position'」;:68,搜「Position 1」)。定义、实例化与运行输出均见原文。 ↩↩

  3. 出处:「SECTION IV: Data Collections」第 52–54 段(text/25-fm-section-iv-data-collections.txt:52,搜「index 0 for x and 1 for y」)。原文:「you need to use the index 0 for x and 1 for y, which isn't as intuitive and may lead to confusion in larger programs」。 ↩

  4. 出处:「SECTION IV: Data Collections」第 29–36 段(text/25-fm-section-iv-data-collections.txt:30,搜「fields can be accessed using names」;:34,搜「without the overhead」)。「retaining the immutability and lightweight nature of regular tuples」见篇首第 21–24 段。 ↩

  5. 出处:「SECTION IV: Data Collections」第 74–105 段(text/25-fm-section-iv-data-collections.txt:83,搜「'Package'」;:90,搜「package1.sender」;:94,搜「package1[0]」)。两种取法并排的原文见第 89–95 段,运行结果见第 98–101 段。 ↩↩

  6. 出处:「SECTION IV: Data Collections」第 33–36 段(text/25-fm-section-iv-data-collections.txt:34,搜「without the overhead」)。原文:「typically used to represent simple data structures without the overhead of a full class」。 ↩

  7. 出处:「SECTION IV: Data Collections」第 133–143 段(text/25-fm-section-iv-data-collections.txt:136,搜「_replace」;:137,搜「_fields」)。这些进阶工具只出现在「INTERACT WITH GENAI」的提问清单里,正文未展开。 ↩↩

  8. 出处:「SECTION IV: Data Collections」第 144–148 段(text/25-fm-section-iv-data-collections.txt:146,搜「official documentations」)。章末指向 Python 官方文档 collections.namedtuple 一节。 ↩

  9. 出处:「SECTION IV: Data Collections」第 107–125 段(text/25-fm-section-iv-data-collections.txt:114,搜「'Car'」;:117,搜「Toyota」;:124,搜「Car 1」)。汽车档案案例的定义、实例化与运行输出。 ↩

  10. 出处:「SECTION IV: Data Collections」第 133–143 段(text/25-fm-section-iv-data-collections.txt:139,搜「unpacking」)。解包只出现在提问清单:「Demonstrate how to unpack a namedtuple using tuple unpacking」。 ↩

  11. 出处:「SECTION IV: Data Collections」第 21–24 段(text/25-fm-section-iv-data-collections.txt:22,搜「meaningful names」)。篇首论点:「assign meaningful names to each element while retaining the immutability and lightweight nature of regular tuples…improving code clarity and reducing the risk of errors in larger projects」。 ↩