← 返回题目列表

Python 的 namedtuple 是什么?和普通 tuple、dataclass 有什么区别?

中等 第 18 / 21 题 更新于 2026/07/31
Pythonnamedtuplecollections元组

简化版

**collections.namedtuple 是「带字段名的元组」——它工厂式地创建一个 tuple 的子类,让你既能用「下标」也能用「字段名(属性)」访问元素,兼具元组的不可变、轻量、可哈希,又有「像对象一样按名字取值」的可读性。**用法:Point = namedtuple('Point', ['x', 'y']),然后 p = Point(1, 2),可以 p.x/p.y(按名)或 p[0]/p[1](按下标),还能解包 x, y = p核心价值:把「一堆位置含义靠注释记忆的元组」(return (name, age, city) 用的时候要记住第几个是啥)变成「自解释的记录」(return Person(name, age, city),用 p.age 一目了然)。特点:① 不可变(和 tuple 一样,不能改字段,改要用 _replace() 返回新的);② 轻量(比普通类/字典省内存,基于 tuple);③ 有 _fields_asdict()_make() 等辅助方法。vs dataclass:namedtuple 不可变、是元组(能解包/比较/当字典键)、轻量;dataclass 默认可变、更灵活(可加方法、默认值、类型注解友好),需要可变或复杂逻辑用 dataclass。核心记忆:namedtuple 是带字段名的不可变元组,p.xp[0] 都行,比裸元组可读、比类/字典轻量;要可变用 dataclass。

详细版

namedtuple 的用法

操作说明
namedtuple('Name', ['a','b'])创建类型
p.a / p[0]按名 / 按下标访问
a, b = p解包
p._replace(a=9)返回改了字段的新实例(不可变)
p._asdict()转成 dict
Point._fields字段名元组
Point._make(iterable)从可迭代对象创建
from collections import namedtuple

# 创建类型
Point = namedtuple('Point', ['x', 'y'])
# 或 namedtuple('Point', 'x y')  也行(空格/逗号分隔的字符串)

p = Point(1, 2)
print(p.x, p.y)         # 1 2(按字段名)
print(p[0], p[1])       # 1 2(按下标,仍是元组)
x, y = p                # 解包
print(p)                # Point(x=1, y=2)(可读的 repr)

# 不可变(和 tuple 一样)
# p.x = 10              # ✗ AttributeError(不能改)
p2 = p._replace(x=10)  # ✓ 返回新实例 Point(x=10, y=2)

# 辅助方法
print(Point._fields)   # ('x', 'y')
print(p._asdict())     # {'x': 1, 'y': 2}
p3 = Point._make([3, 4])   # 从可迭代对象 → Point(x=3, y=4)

# 默认值(Python 3.7+)
Node = namedtuple('Node', ['val', 'next'], defaults=[None])
Node(5)                # Node(val=5, next=None)

# 当字典键(可哈希,因为是元组)
d = {Point(0,0): "origin"}

# 对比裸元组的可读性问题
def get_user(): return ("Tom", 25, "NYC")   # 用的时候要记住顺序
u = get_user(); u[1]                        # 25 是什么?要看定义
User = namedtuple('User', 'name age city')
def get_user2(): return User("Tom", 25, "NYC")
u = get_user2(); u.age                      # 一目了然

⚠️ namedtuple 的核心价值是「给元组的每个位置起个名字」——它解决了裸元组「靠位置记忆含义」的可读性痛点,同时完整保留元组的所有优点(不可变、可哈希、可解包、轻量、可比较)。想象一个函数返回 (name, age, city),调用方拿到后 data[1] 是什么全靠记忆或注释,改了顺序就出隐蔽 bug;换成 namedtupledata.age 自解释、顺序无关。它本质是「tuple 的子类」,所以 isinstance(p, tuple) 为真、能当字典键、能和元组比较(Point(1,2) == (1,2) 为 True)、内存比字典/普通对象小。关键限制是不可变——想「改一个字段」得用 p._replace(field=new) 生成一个新实例。选型上:只读的、轻量的「记录/数据点」用 namedtuple;需要可变、要加方法/校验/默认值、字段多且逻辑复杂的用 dataclass@dataclass,3.7+);需要极致性能和不可变还可以看 typing.NamedTuple(带类型注解的 namedtuple 写法)。

完整版教学

一、namedtuple 是什么

先理解 namedtuple 的定位:

collections.namedtuple:创建"带字段名的元组"类型
  是一个"工厂函数"——生成一个 tuple 的子类

创建:
  namedtuple(类型名, 字段名列表)
  Point = namedtuple('Point', ['x', 'y'])
  Point = namedtuple('Point', 'x y')       # 字符串也行(空格分隔)
  Point = namedtuple('Point', 'x, y')      # 逗号也行

使用:
  p = Point(1, 2)          # 像类一样实例化
  p.x, p.y                 # 按字段名访问(新增能力)
  p[0], p[1]               # 按下标访问(元组本色)
  x, y = p                 # 解包(元组本色)

本质:tuple 的子类
  isinstance(p, tuple) → True
  → 拥有元组的一切:不可变、可哈希、可比较、可解包、轻量
  → 额外多了"按字段名访问"和"可读的 repr"

解决的痛点:
  裸元组 (a, b, c) 靠"位置"记含义 → 可读性差、易错
  namedtuple 给每个位置起名 → 自解释、顺序无关取值

repr 更友好:
  普通元组:(1, 2)
  namedtuple:Point(x=1, y=2)  ← 一眼看懂

所以 namedtuple 是带字段名的 tuple 子类,既能 p.x 又能 p[0],可读+保留元组优点

collections.namedtuple 创建「带字段名的元组」类型——是一个「工厂函数」、生成一个 tuple 的子类。创建namedtuple(类型名, 字段名列表)Point = namedtuple('Point', ['x','y']),字段名也可用空格/逗号分隔的字符串)。使用p = Point(1, 2) 实例化、p.x/p.y(按字段名)、p[0]/p[1](按下标)、x, y = p(解包)。本质:tuple 的子类isinstance(p, tuple) 为 True、拥有元组一切:不可变、可哈希、可比较、可解包、轻量、额外多了按字段名访问和可读 repr)。解决的痛点:裸元组靠「位置」记含义(可读性差、易错)、namedtuple 给每个位置起名(自解释、顺序无关取值)。repr 更友好:Point(x=1, y=2)(1,2) 一眼看懂。理解「namedtuple 是带字段名的 tuple 子类(工厂函数生成);既能 p.x 又能 p[0]、能解包;本质 tuple 子类保留元组优点;解决裸元组靠位置记含义的可读性痛点;repr 友好」,就理解了 namedtuple 是什么。

二、访问、解包、不可变

理解 namedtuple 的核心行为:

访问方式(两种,都可用):
  p.x        # 按字段名(可读)
  p[0]       # 按下标(元组本色)
  → 同一个值两种访问,随场景选

解包(元组本色):
  x, y = p               # 全部解包
  first, *rest = p       # 星号解包
  → 完全兼容元组解包

不可变(关键限制):
  p.x = 10               # ✗ AttributeError(不能赋值)
  p[0] = 10              # ✗ TypeError(元组不可变)
  → 和 tuple 一样,创建后不能改

想"改字段" → _replace(返回新实例):
  p2 = p._replace(x=10)  # p2 = Point(x=10, y=2),p 不变
  → 不可变对象的"修改" = 生成新对象

可比较(元组本色):
  Point(1,2) == Point(1,2)   # True(按值比较)
  Point(1,2) == (1,2)        # True(和普通元组比也相等!)
  Point(1,2) < Point(1,3)    # True(按字段顺序逐个比)
  → 支持排序(sorted 一堆 namedtuple)

可哈希 → 能当字典键/集合元素:
  d = {Point(0,0): "origin"}     # ✓(元组可哈希)
  s = {Point(1,1), Point(2,2)}   # ✓

所以 p.x/p[0]两种访问、可解包、不可变(改用 _replace)、可比较、可哈希当键

访问方式(两种都可用)p.x(按字段名、可读)、p[0](按下标、元组本色)。解包(元组本色)x, y = p(全部)、first, *rest = p(星号),完全兼容元组解包。不可变(关键限制)p.x = 10 ✗(AttributeError)、p[0] = 10 ✗(元组不可变)。想「改字段」→ _replace(返回新实例)p2 = p._replace(x=10)(p2 是新的、p 不变,不可变对象的「修改」= 生成新对象)。可比较(元组本色)Point(1,2) == Point(1,2) 为 True、Point(1,2) == (1,2) 为 True(和普通元组比也相等)、Point(1,2) < Point(1,3)(按字段顺序逐个比、支持排序)。可哈希 → 能当字典键/集合元素。理解「访问 p.x/p[0]两种、解包兼容元组、不可变(改用 _replace 返回新实例)、可比较(和普通元组比也相等、支持排序)、可哈希当字典键」,就掌握了核心行为。

三、辅助方法与默认值

理解 namedtuple 的辅助方法:

辅助方法(都以 _ 开头,避免和字段名冲突):

_fields:字段名元组
  Point._fields  → ('x', 'y')
  用途:反射、遍历字段

_asdict():转成字典
  p._asdict()  → {'x': 1, 'y': 2}
  用途:序列化、和需要 dict 的接口对接

_make(iterable):从可迭代对象创建
  Point._make([3, 4])  → Point(x=3, y=4)
  用途:从 CSV 行、数据库行批量构造
  rows = [Point._make(row) for row in csv_data]

_replace(**kwargs):改字段返回新实例
  p._replace(x=10)  → Point(x=10, y=2)

为什么方法都带下划线:
  避免和"用户定义的字段名"冲突
  (如果字段叫 fields,就不会撞上 _fields)

默认值(Python 3.7+):
  Node = namedtuple('Node', ['val', 'next'], defaults=[None])
  → defaults 从右往左对应字段
  Node(5)      → Node(val=5, next=None)
  Node(5, 6)   → Node(val=5, next=6)

  多个默认值:
  C = namedtuple('C', 'a b c', defaults=[1, 2])
  → b、c 有默认(从右对齐),a 必填

字段名规则:
  ① 合法标识符、不能是关键字
  ② 不能重复、不能以下划线开头
  (非法名可用 rename=True 自动改成 _0、_1...)

所以辅助方法:_fields/_asdict/_make/_replace(带下划线避免冲突)、defaults 设默认值

辅助方法(都以 _ 开头、避免和字段名冲突)_fields(字段名元组 Point._fields('x','y')、用于反射遍历);_asdict()(转成字典 p._asdict(){'x':1,'y':2}、用于序列化/对接需要 dict 的接口);_make(iterable)(从可迭代对象创建 Point._make([3,4])、用于从 CSV 行/数据库行批量构造);_replace(**kwargs)(改字段返回新实例)。为什么方法带下划线:避免和用户定义的字段名冲突。默认值(Python 3.7+)namedtuple('Node', ['val','next'], defaults=[None])——defaults 从右往左对应字段(Node(5)Node(val=5, next=None))。字段名规则:合法标识符、不能是关键字/重复/下划线开头(非法名用 rename=True 自动改)。理解「辅助方法 _fields/_asdict/_make/_replace(带下划线避免撞字段名);defaults 设默认值(从右往左);字段名要合法标识符(非法用 rename=True)」,就掌握了辅助方法与默认值。

四、vs 裸元组、字典

理解 namedtuple 相比裸元组和字典的取舍:

namedtuple vs 裸元组:
  裸元组 (name, age, city):
    ✗ 靠位置记含义(data[1] 是什么?)
    ✗ 改顺序 → 隐蔽 bug
    ✓ 轻量
  namedtuple:
    ✓ 按名访问(data.age 自解释)
    ✓ 顺序无关取值、可读 repr
    ✓ 同样轻量、不可变
  → namedtuple 全面优于裸元组(做"记录"时)

namedtuple vs 字典:
  字典 {'name':..., 'age':...}:
    ✓ 可变、灵活(随时加删键)
    ✗ 可变(可能被意外改)
    ✗ 内存大(哈希表开销)
    ✗ 不能解包成 a,b、不能当另一个字典的键
  namedtuple:
    ✓ 不可变(安全)、轻量(省内存)
    ✓ 可解包、可哈希(当字典键)、可比较排序
    ✗ 字段固定(创建后结构不变)
  → 固定结构的只读记录用 namedtuple,动态键值用字典

内存对比(直观):
  namedtuple(基于 tuple)< 普通对象(有 __dict__)
  namedtuple < 字典(哈希表开销)
  → 大量小记录用 namedtuple 明显省内存

选择:
  固定字段、只读、要轻量/可哈希 → namedtuple
  动态键、要可变 → 字典
  位置就够、临时用 → 裸元组

所以 namedtuple 优于裸元组(可读)、比字典轻量+不可变+可哈希(但字段固定)

namedtuple vs 裸元组:裸元组靠位置记含义(data[1] 是什么?改顺序出隐蔽 bug)、namedtuple 按名访问(data.age 自解释、顺序无关、可读 repr、同样轻量不可变)——namedtuple 全面优于裸元组(做记录时)namedtuple vs 字典:字典可变灵活(随时加删键)但可变(可能被意外改)、内存大(哈希表)、不能解包/当字典键;namedtuple 不可变(安全)、轻量(省内存)、可解包、可哈希(当字典键)、可比较排序,但字段固定——固定结构的只读记录用 namedtuple、动态键值用字典内存对比:namedtuple(基于 tuple)< 普通对象(有 __dict__)< 字典(哈希表),大量小记录用 namedtuple 明显省内存。理解「namedtuple 优于裸元组(可读、顺序无关);比字典轻量+不可变(安全)+可哈希(当键)+可解包(但字段固定);固定只读记录用 namedtuple、动态键用字典」,就掌握了这个取舍。

五、vs dataclass、typing.NamedTuple

理解 namedtuple 和 dataclass 的选型:

namedtuple vs dataclass(@dataclass,3.7+):

namedtuple:
  ✓ 不可变(默认)、是元组(可解包/哈希/比较/当键)
  ✓ 最轻量(基于 tuple)
  ✗ 加方法/复杂逻辑不方便
  ✗ 字段都得有值(除非 defaults)

dataclass:
  @dataclass
  class Point:
      x: int
      y: int = 0        # 默认值方便
  ✓ 默认可变(也可 frozen=True 变不可变)
  ✓ 类型注解友好、能加方法、__post_init__ 校验
  ✓ 灵活(继承、字段元数据、比较可配)
  ✗ 不是元组(不能解包成 a,b,要 astuple())
  ✗ 比 namedtuple 略重

typing.NamedTuple(带注解的 namedtuple 写法):
  class Point(NamedTuple):
      x: int
      y: int = 0
  → 等价 namedtuple 但用类语法+类型注解,能加方法
  → "现代版 namedtuple"(推荐用它替代老 namedtuple 写法)

选择:
  不可变记录 + 要元组特性(解包/哈希/当键) → namedtuple / typing.NamedTuple
  可变 / 要方法/校验/继承 → dataclass
  可变+要不可变选项 → dataclass(frozen=True)
  纯轻量只读数据点 → namedtuple

现代建议:
  简单不可变记录:typing.NamedTuple(类语法+注解)
  需要可变/复杂:dataclass

所以 namedtuple 不可变+元组特性、dataclass 可变+灵活;现代用 typing.NamedTuple/dataclass

namedtuple vs dataclass(@dataclass,3.7+)namedtuple 不可变(默认)、是元组(可解包/哈希/比较/当键)、最轻量,但加方法/复杂逻辑不方便;dataclass 默认可变(可 frozen=True 变不可变)、类型注解友好、能加方法、__post_init__ 校验、灵活(继承/字段元数据),但不是元组(不能解包成 a,b、要 astuple())、略重。typing.NamedTuple(带注解的 namedtuple 写法)class Point(NamedTuple): x: int; y: int = 0——等价 namedtuple 但用类语法+类型注解、能加方法(「现代版 namedtuple」推荐替代老写法)。选择:不可变记录+要元组特性用 namedtuple/typing.NamedTuple、可变/要方法校验继承用 dataclass、可变+要不可变选项用 dataclass(frozen=True)。理解「namedtuple 不可变+元组特性(解包/哈希/当键)最轻量、dataclass 可变+灵活(方法/校验/继承)但不是元组;typing.NamedTuple 是现代版(类语法+注解);不可变记录用 NamedTuple、可变复杂用 dataclass」,就掌握了选型。

六、总结与实践

总结 namedtuple 的使用:

namedtuple 速查:
  创建:Point = namedtuple('Point', ['x','y'])
  访问:p.x(按名)/ p[0](按下标)
  解包:x, y = p
  改:  p._replace(x=9)(返回新的)
  辅助:_fields、_asdict()、_make(iter)
  默认:namedtuple('N', 'a b', defaults=[0])(3.7+)

核心优点:
  ① 可读(按字段名,比裸元组好)
  ② 轻量(基于 tuple,比字典/对象省内存)
  ③ 不可变(安全)+ 可哈希(当字典键)
  ④ 可解包、可比较(和普通元组兼容)

关键限制:
  不可变(改用 _replace)、字段固定(结构不变)

典型场景:
  函数返回多值的"命名记录"(return Result(...))
  数据点/坐标/RGB 颜色/数据库行
  需要当字典键的复合键

选型:
  只读轻量记录 → namedtuple / typing.NamedTuple(现代)
  可变/要方法 → dataclass
  动态键 → dict
  临时位置 → 裸元组

核心总结:
  namedtuple = 带字段名的不可变元组
  p.x 和 p[0] 都行,可读+轻量+可哈希
  改用 _replace,可变用 dataclass

所以 namedtuple 带字段名的不可变元组,可读+轻量+可哈希,可变用 dataclass

namedtuple 速查:创建 namedtuple('Point', ['x','y'])、访问 p.x/p[0]、解包 x,y = p、改 p._replace(x=9)、辅助 _fields/_asdict()/_make(iter)、默认 defaults=[0](3.7+)。核心优点:① 可读(按字段名)、② 轻量(基于 tuple 省内存)、③ 不可变(安全)+ 可哈希(当字典键)、④ 可解包、可比较。关键限制:不可变(改用 _replace)、字段固定。典型场景:函数返回多值的命名记录、数据点/坐标/RGB/数据库行、需要当字典键的复合键。选型:只读轻量记录用 namedtuple/typing.NamedTuple、可变/要方法用 dataclass、动态键用 dict。理解「namedtuple 带字段名的不可变元组、p.x 和 p[0]都行、可读+轻量+可哈希+可解包;改用 _replace;场景命名记录/数据点/复合键;选型只读用 namedtuple、可变用 dataclass」,就掌握了总结与实践。

记忆钩子:「collections.namedtuple 是『带字段名的元组』——工厂函数创建一个 tuple 的子类,让你既能按字段名(p.x)又能按下标(p[0])访问,还能解包(x,y=p);创建 Point=namedtuple(‘Point’,[‘x’,‘y’]),实例 Point(1,2)的 repr 是 Point(x=1,y=2)(可读);★本质是 tuple 子类,保留元组所有优点:不可变、可哈希(能当字典键)、可比较(Point(1,2)==(1,2)为 True、支持排序)、可解包、轻量(比字典/普通对象省内存);解决裸元组『靠位置记含义』的痛点(data[1]是啥→data.age 自解释、改顺序不出 bug);关键限制:不可变——想改字段用 p._replace(x=9)返回新实例;辅助方法(带下划线避免撞字段名):_fields(字段名)、_asdict()(转字典)、_make(iterable)(从序列构造)、defaults=[…]设默认值(3.7+,从右往左);★vs dataclass:namedtuple 不可变+是元组(解包/哈希/当键)+最轻量、dataclass 默认可变+更灵活(加方法/校验/默认值/继承)但不是元组;现代写法用 typing.NamedTuple(类语法+类型注解);选型:只读轻量记录用 namedtuple、可变或复杂用 dataclass、动态键用 dict」

七、常见误区与追问

  • 误区:namedtuple 创建的实例可以修改字段。 不能——namedtuple 是 tuple 的子类、和元组一样不可变,p.x = 10 会抛 AttributeError、p[0] = 10 会抛 TypeError;想「修改某个字段」要用 p._replace(x=10),它返回一个改了字段的新实例、原实例不变(这是不可变对象的通用做法:修改 = 生成新对象);需要可变的记录应该用 dataclass。
  • 误区:namedtuple 和字典一样,可以随时增删字段。 不行——namedtuple 的字段在创建类型时就固定了(namedtuple('Point', ['x','y']) 定死了 x、y),实例不能加新字段也不能删字段(它是元组、结构固定);字典才能动态增删键;如果数据结构是固定的(如坐标、数据库行)用 namedtuple 更合适(可读+轻量+安全),结构动态变化才用字典。
  • 误区:namedtuple 比普通元组占用更多内存。 不会——namedtuple 基于 tuple、每个实例的内存和普通元组几乎一样(字段名存在类型上、不在每个实例里,实例本身还是 tuple),比字典(有哈希表开销)和普通对象(有 __dict__)都省内存;这也是它适合「大量小记录」的原因;它多出来的只是「按名访问」的便利和可读的 repr,几乎不增加实例开销。
  • 误区:namedtuple 和普通元组不能比较。 能比较、而且相等——namedtuple 是 tuple 子类、按值比较,Point(1,2) == Point(1,2) 为 True,甚至 Point(1,2) == (1,2) 也为 True(和普通元组比较时只看值);也支持大小比较和排序(Point(1,2) < Point(1,3) 按字段顺序逐个比),可以直接 sorted(list_of_points);这是元组特性的完整继承。
  • 追问:namedtuple 相比裸元组和字典,各有什么优劣?该怎么选? 相比裸元组:namedtuple 解决了「靠位置记含义」的可读性痛点(p.age vs t[1]、改顺序不易出 bug),同时保留元组的所有优点(不可变、可哈希、可解包、轻量),做「记录」时全面优于裸元组。相比字典:namedtuple 不可变(更安全、不会被意外改)、更省内存、可解包、可哈希(能当字典键或集合元素),但字段固定(不能动态增删);字典可变灵活但更重、不能解包/当键。选择:固定结构的只读记录(坐标、颜色、数据库行、函数返回多值)用 namedtuple;结构动态、需要频繁增删键或修改值用字典;只是临时用位置就够、不需要名字用裸元组。
  • 追问:namedtuple 的 _replace_asdict_make 方法分别做什么?为什么带下划线? _replace(**kwargs):返回一个把指定字段替换成新值的新实例(因为 namedtuple 不可变、不能原地改),如 p._replace(x=10)_asdict():把实例转成字典({'x':1,'y':2}),方便序列化或对接需要 dict 的接口;_make(iterable):从一个可迭代对象(如 CSV 的一行、数据库查询的一行)批量构造实例,如 Point._make([3,4]),常用于 [Point._make(row) for row in rows];另外 _fields 是字段名元组、_field_defaults 是默认值。这些方法/属性都带下划线前缀,是为了避免和「用户自定义的字段名」冲突——如果一个字段恰好叫 fieldsmake,普通命名就会撞车,用下划线前缀就能区分开。
  • 追问:namedtuple 和 dataclass 怎么选?typing.NamedTuple 又是什么? namedtuple 适合「不可变、轻量、需要元组特性(可解包/可哈希/可当字典键/可比较排序)」的只读记录;dataclass(@dataclass)适合「需要可变(默认可变,也能 frozen=True 变不可变)、要加方法/校验(__post_init__)/默认值/继承、类型注解友好」的场景,但它不是元组(不能直接解包成 a,b、要用 astuple())、略重。typing.NamedTuple 是 namedtuple 的「现代写法」——用类语法加类型注解定义(class Point(NamedTuple): x: int; y: int = 0),底层生成的还是 namedtuple(不可变元组、有所有元组特性),但写法更清晰、支持类型注解、还能定义方法,推荐用它替代老式的 namedtuple('Point', ...) 函数调用写法。选型口诀:不可变记录用 typing.NamedTuple、可变或复杂逻辑用 dataclass、动态键值用 dict。

八、加强记忆

collections.namedtuple 是「带字段名的元组」——工厂函数创建一个 tuple 的子类,让你既能按字段名(p.x)又能按下标(p[0])访问、还能解包(x, y = p);创建 Point = namedtuple('Point', ['x','y']),实例 Point(1,2) 的 repr 是 Point(x=1, y=2)(可读)。本质是 tuple 子类,保留元组所有优点:不可变、可哈希(能当字典键)、可比较(Point(1,2) == (1,2) 为 True、支持排序)、可解包、轻量(比字典/普通对象省内存);解决裸元组「靠位置记含义」的痛点(data[1] 是啥 → data.age 自解释、改顺序不出 bug)。关键限制:不可变——想改字段用 p._replace(x=9) 返回新实例。辅助方法(带下划线避免撞字段名)_fields(字段名)、_asdict()(转字典)、_make(iterable)(从序列构造)、defaults=[...] 设默认值(3.7+、从右往左)。vs dataclass:namedtuple 不可变 + 是元组(解包/哈希/当键)+ 最轻量;dataclass 默认可变 + 更灵活(加方法/校验/默认值/继承)但不是元组;现代写法用 typing.NamedTuple(类语法 + 类型注解)。一句话「namedtuple 带字段名的不可变元组(tuple 子类),p.x 和 p[0]都行、可解包、可哈希当键、可比较、轻量,比裸元组可读、比字典省内存;改字段用 _replace 返回新实例;辅助 _fields/_asdict/_make/defaults;要可变或加方法用 dataclass、现代写法 typing.NamedTuple」。