Python tuple 有什么特点?元组拆包有哪些常见用法?
简化版
tuple 是有序、不可变的序列,常用于表达固定结构的数据,比如坐标、函数多返回值。元组拆包可以把序列中的元素一次性绑定到多个变量,还支持 * 收集剩余元素。
详细版
基本用法:
point = (10, 20)
x, y = point
print(x, y) # 10 20
函数多返回值本质上常见就是返回 tuple:
def get_user():
return 1, "Tom"
user_id, name = get_user()
扩展拆包:
nums = (1, 2, 3, 4, 5)
first, *middle, last = nums
print(first) # 1
print(middle) # [2, 3, 4]
print(last) # 5
注意:
- 单元素元组必须写逗号:
(1,)。 tuple自身不可变,但内部引用的可变对象仍然可能被修改。- 只有所有元素都可哈希时,tuple 才能作为 dict key。
完整版教学
一、tuple 的不可变指的是什么
tuple 创建后,不能替换、增加或删除元素:
t = (1, 2, 3)
# t[0] = 100 # TypeError
但如果 tuple 里放的是可变对象,可变对象本身仍然可以改:
t = ([1, 2], "python")
t[0].append(3)
print(t) # ([1, 2, 3], 'python')
这里 tuple 的第一个位置仍然引用同一个列表对象,没有替换 tuple 的元素;变化发生在列表内部。
可以这样理解:
t = ([1, 2], "python")
tuple 槽位 0 ──> list 对象 [1, 2]
tuple 槽位 1 ──> str 对象 "python"
槽位不能换,但槽位 0 指向的 list 自己可以 append
所以“tuple 不可变”不是深度不可变。它保证的是 tuple 自身的长度和每个位置的引用不变,不保证内部引用对象的内容不变。
二、为什么单元素元组要加逗号
这点很高频:
a = (1)
b = (1,)
print(type(a)) # <class 'int'>
print(type(b)) # <class 'tuple'>
括号本身不决定 tuple,逗号才是关键。(1) 只是给表达式加括号,(1,) 才是单元素 tuple。
甚至不写括号也可以创建 tuple:
t = 1, 2, 3
print(type(t)) # <class 'tuple'>
括号更多是为了分组和可读性;真正让表达式成为 tuple 的是逗号。这也是函数多返回值的基础:return a, b 实际返回的是一个 tuple。
三、元组拆包让代码更清晰
交换变量:
a, b = b, a
遍历键值对:
user = {"name": "Tom", "age": 18}
for key, value in user.items():
print(key, value)
函数返回多个值:
def divide(a, b):
return a // b, a % b
quotient, remainder = divide(10, 3)
这些写法背后都利用了序列拆包。
拆包要求左边变量数量和右边元素数量匹配,否则会报 ValueError。这能帮助你尽早发现数据结构不符合预期,而不是悄悄丢字段。比如处理 (status, data) 这种固定结构时,拆包比 result[0]、result[1] 更表达语义。
pair = (200, "ok")
status, message = pair
四、星号拆包处理不定长度
当序列长度不固定时,可以用 * 接住一部分:
head, *tail = [1, 2, 3, 4]
print(head) # 1
print(tail) # [2, 3, 4]
也可以放中间:
first, *body, last = [1, 2, 3, 4]
print(body) # [2, 3]
注意,被 * 接住的部分是 list,不是 tuple。
数字例子:first, *middle, last = (1, 2, 3, 4, 5) 中,first 得到 1,last 得到 5,中间剩下 3 个元素被收集成列表 [2, 3, 4]。一个拆包表达式里只能有一个带星号目标,否则 Python 不知道如何分配剩余元素。
记忆钩子:普通拆包要求数量精确匹配,星号拆包给“不确定长度的中间部分”留了一个收集口。
五、tuple 什么时候适合做 dict key
tuple 可以作为 dict key 的前提是它本身可哈希,而这要求内部元素也可哈希。
可以:
locations = {}
locations[(10, 20)] = "A"
不可以:
# d = {([1, 2], 3): "bad"} # TypeError
因为列表不可哈希,包含列表的 tuple 也不可哈希。
常见用法是坐标、复合 key、多字段索引:
scores = {}
scores[("class-1", "Tom")] = 95
scores[("class-1", "Jerry")] = 88
这里 tuple 表示固定结构的两个字段,顺序有意义:("class-1", "Tom") 和 ("Tom", "class-1") 不是同一个 key。如果组合不关心顺序,用 frozenset 可能更合适。
六、tuple 和 list 怎么选
tuple 更适合固定结构,list 更适合同质、可变的一组元素。比如坐标 (x, y)、颜色 (r, g, b)、函数返回 (status, payload) 都是 tuple 的典型场景;用户列表、任务队列、可增删的结果集更适合 list。不要因为 tuple “性能可能更好一点”就到处用 tuple,语义优先。
| 场景 | 更适合 | 原因 |
|---|---|---|
坐标 (x, y) | tuple | 固定字段结构 |
| 函数多返回值 | tuple | 天然可拆包 |
| 动态收集数据 | list | 需要增删改 |
| dict 复合 key | tuple | 不可变且可哈希条件更容易满足 |
七、常见误区与追问
- 误区:
(1)是单元素 tuple。 这是带括号的整数表达式,单元素 tuple 必须写(1,)。 - 误区:tuple 内部任何内容都不能变。 tuple 槽位不可变,但槽位指向的 list 等可变对象仍能变。
- 误区:星号拆包接住的是 tuple。 带星号目标接住的是 list。
- 追问:函数多返回值本质是什么? 常见情况下是返回一个 tuple,调用方再拆包。
- 追问:tuple 什么时候不能做 dict key? 当内部包含 list、dict、set 等不可哈希对象时,整个 tuple 不可哈希。
- 追问:tuple 和 list 怎么选? 固定结构、不可变语义、复合 key 用 tuple;动态增删的一组数据用 list。
八、加强记忆
把 tuple 记成“固定结构的有序记录”:逗号才是 tuple 的关键,单元素要写 (x,);拆包让固定结构的字段绑定更清楚,星号拆包处理不定长度但接住 list。tuple 是否能做 dict key 取决于内部元素是否都可哈希,内部可变对象仍可能被修改,这两个边界最容易被追问。