Python 的 namedtuple 是什么?和普通 tuple、dataclass 有什么区别?
简化版
**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.x 和 p[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;换成namedtuple,data.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.agevst[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是默认值。这些方法/属性都带下划线前缀,是为了避免和「用户自定义的字段名」冲突——如果一个字段恰好叫fields或make,普通命名就会撞车,用下划线前缀就能区分开。 - 追问: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」。