前言
元组(tuple)是 Python 的内置序列类型之一,和列表长得很像,区别只有一个:元组不可变。创建之后不能增、不能删、不能改,任何看起来在「修改」元组的操作,实际都是新建了一个元组。正因为不可变,元组可以计算哈希值(前提是里面每个元素都可哈希),于是能当字典的键、能放进集合,这是列表做不到的。
元组的语法有一个著名的陷阱:逗号才是元组的标志,圆括号不是。t = 1, 2, 3是元组,t = (1)是整数,t = (1,)才是只含一个元素的元组。很多人被这一点绊过。
本文先讲元组的基本概念,再系统盘点它的常用操作:索引切片、count/index、拼接与重复、解包、遍历、排序与转换,并配一份完整的可运行示例和坑点清单。
一、创建元组:逗号是关键
官方文档对元组的定义是「用逗号分隔的多个值组成的序列」,圆括号是可选的、主要用于分组。所以下面几种写法都成立。
# 适用于 Python 3.8+
t1 = 1, 2, 3 # 不加括号
t2 = (1, 2, 3) # 加括号
t3 = () # 空元组
t4 = (1,) # 单元素元组,逗号不能省
t5 = tuple([1, 2, 3]) # 从可迭代对象转换
t6 = tuple("abc") # ('a', 'b', 'c')
print(t4, len(t4)) # (1,) 1
print(type((1)).__name__) # int,这个是整数
print(type((1,)).__name__) # tuple,这个才是元组
print(t6) # ('a', 'b', 'c')最后两行的对比值得记牢:(1)只是一对起分组作用的括号,里面的1依然是整数;(1,)带上逗号才构成元组。空元组是例外,()没有元素所以不需要逗号区分。
tuple(iterable)的构造器可以吃任何可迭代对象,字符串会被逐字符拆开,字典会被拆成键。
# 适用于 Python 3.8+
print(tuple({"a": 1, "b": 2})) # ('a', 'b'),只取键
print(tuple(range(3))) # (0, 1, 2)二、索引、切片与成员判断
元组是序列,支持所有通用序列操作:按下标取、切片、len、in、min、max、sum、迭代。
# 适用于 Python 3.8+
t = (10, 20, 30, 40, 50)
print(t[0], t[-1]) # 10 50
print(t[1:4]) # (20, 30, 40),切片返回新元组
print(t[::-1]) # (50, 40, 30, 20, 10)
print(len(t)) # 5
print(30 in t) # True
print(max(t), min(t)) # 50 10
print(sum(t)) # 150
for value in t:
pass # 可以正常迭代有一个和列表共通的坑:切片不会越界报错,但按下标访问会。t[1:100]返回从 1 到末尾的元组,而t[100]直接抛IndexError。
元组没有.append、.remove、.pop、.sort、.reverse这些方法,因为它们都会修改内容。写t.append(1)会得到AttributeError: 'tuple' object has no attribute 'append'。
三、count 与 index
元组只有两个专属方法,官方签名是:
sequence.count(value, /):返回value出现的次数。sequence.index(value[, start[, stop]]):返回value第一次出现的下标,可以限定搜索的起止区间;找不到抛ValueError。
# 适用于 Python 3.8+
t = (1, 2, 2, 3, 2, 4)
print(t.count(2)) # 3
print(t.count(99)) # 0,不存在返回 0,不报错
print(t.index(2)) # 1,第一次出现的下标
# index 可以限定区间
print(t.index(2, 2)) # 2,从下标 2 开始找
try:
t.index(99)
except ValueError as e:
print("报错:", e) # tuple.index(x): x not in tuplecount找不到返回0,而index找不到抛异常——这两个方法对「不存在」的处理方式相反,是很容易记混的一点。要安全取值可以先用in判断,或者捕获ValueError。
四、拼接、重复与赋值
+拼接两个元组,*重复元组内容,两者都返回新元组,原元组不变。
# 适用于 Python 3.8+
a = (1, 2)
b = (3, 4)
print(a + b) # (1, 2, 3, 4)
print(a * 2) # (1, 2, 1, 2)
print(a) # (1, 2),a 没有变
print(b) # (3, 4),b 没有变
c = a
c += (9,) # += 对元组是「新建并重新绑定」
print(c) # (1, 2, 9)
print(a) # (1, 2),a 不受影响最后四行是一个重要区别。对列表做+=是原地扩展,所有指向该列表的变量都会看到变化;对元组做+=只能新建一个元组再让变量重新指向它,原来的元组纹丝不动。这个差异在多个变量共享同一对象时非常关键。
# 适用于 Python 3.8+
lst = [1, 2]
alias_list = lst
lst += [3]
print(alias_list) # [1, 2, 3],列表原地改,别名看到变化
tpl = (1, 2)
alias_tuple = tpl
tpl += (3,)
print(alias_tuple) # (1, 2),元组新建,别名看不到变化五、解包:元组最好用的地方
元组支持解包(unpacking),把元素一次性分给多个变量。这是元组在日常代码里出现频率最高的用途。
# 适用于 Python 3.8+
point = (3, 5)
x, y = point
print(x, y) # 3 5
# 交换变量,不需要临时变量
a, b = 1, 2
a, b = b, a
print(a, b) # 2 1
# 用 * 收集剩余元素(3.0 起支持,星号表达式)
first, *middle, last = (1, 2, 3, 4, 5)
print(first) # 1
print(middle) # [2, 3, 4],注意这里是列表
print(last) # 5*middle收集到的是一个列表,不是元组,这一点容易看错。解包时左右两边的元素个数必须匹配,否则抛ValueError: too many values to unpack或not enough values to unpack。
函数返回多个值,本质就是返回一个元组,然后在调用处解包。
# 适用于 Python 3.8+
def min_max(values):
return min(values), max(values) # 返回的是元组
low, high = min_max([3, 1, 4, 1, 5])
print(low, high) # 1 5六、排序与转换
元组自己没有排序方法,需要排序时用内置函数sorted(),它返回排序后的列表。
# 适用于 Python 3.8+
t = (3, 1, 4, 1, 5)
print(sorted(t)) # [1, 1, 3, 4, 5],返回列表
print(list(t)) # [3, 1, 4, 1, 5],转列表
print(tuple(sorted(t))) # (1, 1, 3, 4, 5),再转回元组
print(t) # (3, 1, 4, 1, 5),原元组不变元组之间可以比较大小,规则和列表一致:逐元素比较,遇到第一个不相等的元素就决定结果。
# 适用于 Python 3.8+
print((1, 2) < (1, 3)) # True,第二个元素 2 < 3
print((1, 2) < (1, 2, 0)) # True,前缀相同则短的更小
print((2,) > (1, 9)) # True,第一个元素 2 > 1这个「逐元素比较」的特性让元组常被用作多级排序的键,比如sorted(items, key=lambda item: (item.priority, item.name))。
七、实战:用元组保存不可变记录
# 适用于 Python 3.8+
def split_name(full_name):
"""拆分姓名,返回不可变的 (姓, 名)。"""
parts = full_name.strip().split()
if len(parts) < 2:
return (parts[0], "") if parts else ("", "")
return (parts[0], " ".join(parts[1:]))
def sort_records(records):
"""records 是 (姓名, 分数) 的元组列表,按分数降序、姓名升序排。"""
return sorted(records, key=lambda r: (-r[1], r[0]))
if __name__ == "__main__":
print(split_name("Ada Lovelace")) # ('Ada', 'Lovelace')
print(split_name("Cher")) # ('Cher', '')
data = [("alice", 90), ("bob", 90), ("carol", 85)]
for name, score in sort_records(data):
print(name, score)逐行推演:split_name里parts是列表,返回时用圆括号包成元组,靠逗号构成元组。对于只有一个词的输入,返回(名字, "");空输入返回("", "")。sort_records的排序键是(-r[1], r[0]),先按分数取负升序(等价于分数降序),分数相同时按姓名升序。输出顺序是alice 90、bob 90、carol 85——alice和bob分数相同,按姓名升序排在前面的是alice。最后for name, score in ...对每个元组解包。
常见坑点
坑点 1:单元素元组漏了逗号。❌t = (1),以为它是元组——实际是整数1,len(t)会抛TypeError。 ✅ 写t = (1,),逗号不能省;不加括号的t = 1,也可以。
坑点 2:试图修改元组。❌t = (1, 2); t[0] = 9——抛TypeError: 'tuple' object does not support item assignment。 ✅ 需要修改就新建:t = (9,) + t[1:];频繁修改的场景改用列表。
坑点 3:以为append存在于元组。❌t = (1, 2); t.append(3)——抛AttributeError: 'tuple' object has no attribute 'append'。 ✅ 拼接出新元组:t = t + (3,)。
坑点 4:把+=对元组的行为当成原地修改。❌ 认为t += (1,)会改掉所有指向t的引用——元组不可变,+=是新建并重新绑定,别的引用看不到变化。 ✅ 需要共享修改语义时改用列表;用元组就要接受「重新绑定」。
坑点 5:解包个数不匹配。❌a, b = (1, 2, 3)——抛ValueError: too many values to unpack (expected 2)。 ✅ 用星号接住多余项:a, b, *rest = (1, 2, 3),rest得到[3]。
坑点 6:以为*rest收集到的是元组。❌first, *rest = (1, 2, 3)后认为rest是元组——它实际是一个列表[2, 3]。 ✅ 需要元组就显式转换:rest = tuple(rest)。
坑点 7:混淆count和index找不到时的行为。❌ 用t.count(x)判断是否存在后,又以为t.index(x)在不存在时返回-1——它会抛ValueError。 ✅ 判断存在用x in t;用index时捕获ValueError。
坑点 8:以为元组一定可哈希。❌t = (1, [2, 3]); {t: "x"}——内层列表不可哈希,整个元组随之不可哈希,抛TypeError。 ✅ 保证元组内所有元素可哈希,比如把内层换成(2, 3)或frozenset({2, 3})。
总结
| 操作 | 写法 | 是否新建对象 |
|---|
| 创建 | (1, 2)/1, 2/(1,)/tuple(iterable) | 是 |
| 取值 | t[i]、t[a:b] | 切片是新元组 |
| 计数 | t.count(v) | 否,返回整数 |
| 查下标 | t.index(v[, start[, stop]]) | 否,找不到抛ValueError |
| 拼接 | t + other | 是 |
| 重复 | t * n | 是 |
| 解包 | a, b = t、a, *rest = t | rest是列表 |
| 排序 | sorted(t) | 返回列表 |
| 是否存在 | v in t | 否 |
元组的全部价值来自「不可变」这三个字:不可变带来可哈希,可哈希带来「能当字典键、能进集合」;不可变也带来「共享安全」,多处引用的元组不会被谁偷偷改掉。把元组当结构化的常量来用,而不是当「不能改的列表」来用,才能真正用对。