跳到主要内容

4.2 元组 tuple

上一节的列表随时能增删改,很自由。但有些数据天生就不该被改:一个点的坐标 (3, 5)、你的生日 (2008, 6, 1)、屏幕分辨率 (1920, 1080)。如果用列表存,程序哪个角落不小心 append 或改了一下,bug 就悄悄埋下了。

Python 为这种"定死不动"的数据准备了元组(tuple)——可以简单理解为不可修改的列表。这一节内容不多,但"解包"技巧非常常用。

创建元组

把列表的方括号换成圆括号 (),就是元组:

point = (3, 5)
colors = ("红", "绿", "蓝")
birthday = (2008, 6, 1)

有一个必须记住的特例——只有一个元素时,必须在后面加逗号

single = (5,) # ✅ 这是单元素元组
wrong = (5) # ⚠️ 这不是元组!只是数字 5

print(type(single)) # <class 'tuple'>
print(type(wrong)) # <class 'int'>

为什么?因为 (5) 里的圆括号会被 Python 当成数学括号(就像 (1 + 2) * 3 里那个),真正让 Python 认出元组的其实是逗号。这也是个"不报错的错",类型悄悄不对,后面用起来才发现。

和列表一样的地方

读取类的操作全部通用:索引、切片、len()in、for 遍历,规则一字不差:

colors = ("红", "绿", "蓝")

print(colors[0]) # 红
print(colors[-1]) # 蓝
print(colors[0:2]) # ('红', '绿') 切片结果还是元组
print(len(colors)) # 3
print("绿" in colors) # True

for c in colors:
print(c)

和列表不一样的地方:不可修改

一旦创建,内容就定死了。 所有会"动"到内容的操作统统没有:

colors = ("红", "绿", "蓝")

colors[0] = "紫" # ❌ TypeError: 'tuple' object does not support item assignment
colors.append("黄") # ❌ AttributeError: 'tuple' object has no attribute 'append'
del colors[1] # ❌ TypeError

眼熟吗?TypeError: ... does not support item assignment 正是 2.3 节改字符串时见过的报错。字符串、元组是"不可变家族",列表是"可变家族"——报错信息都在提醒你摸错了家族。

"不能改"反而是优点:

  1. 安全:坐标、生日这种数据用元组存,谁都改不了,从根上杜绝误改
  2. 表达意图:看到元组,读代码的人立刻明白"这组数据是固定搭配"
  3. 元组比列表更省内存、处理更快(现阶段感知不到,知道即可)

选择口诀:内容以后要变,用列表;定死不变,用元组。 拿不准就用列表,不会错,只是少了层保险。

元组解包:本节最实用的技巧

解包(unpacking):把元组里的值一次性分给多个变量:

point = (3, 5)
x, y = point # 解包:x 拿到 3,y 拿到 5

print(x) # 3
print(y) # 5

对比不用解包的写法,高下立判:

x = point[0]
y = point[1]

解包要求左边变量的个数和元组元素个数一致,否则报错:

a, b = (1, 2, 3) # ❌ ValueError: too many values to unpack
a, b, c = (1, 2) # ❌ ValueError: not enough values to unpack

其实你早就见过解包的变体。这几个"老熟人"底层全是元组在工作:

# ① 多变量同时赋值——右边的 1, 2 其实就是省略了括号的元组 (1, 2)
a, b = 1, 2

# ② 交换两个变量——Python 最著名的一行代码,无需中间变量
a, b = b, a
print(a, b) # 2 1

# ③ 上一节 enumerate 的 for i, fruit in ...
# enumerate 每轮产出的是 (序号, 元素) 这样的元组,
# for i, fruit 就是在对它解包!
for i, fruit in enumerate(["苹果", "香蕉"], start=1):
print(i, fruit)

💡 交换写法 a, b = b, a 的原理:先把右边打包成元组 (b的值, a的值),再解包给左边。在其他很多语言里交换变量要借助第三个临时变量,Python 一行搞定。

元组和列表互相转换

tuple()list() 函数(和 int()str() 是一个家族的):

nums = [1, 2, 3]
t = tuple(nums) # 列表 → 元组,"上锁"
print(t) # (1, 2, 3)

back = list(t) # 元组 → 列表,"解锁",得到可修改的副本
back.append(4)
print(back) # [1, 2, 3, 4]

想"修改"元组的正规姿势就是:转列表 → 改 → 转回元组。如果发现自己频繁这么干,说明当初就该用列表。

练习

新建 practice42.py 完成:

1. 创建一个元组存放你的(姓名, 出生年, 城市),用解包取出三个值,f-string 打印成一句自我介绍。

2. 不运行,先猜输出,再验证:

t = (10, 20, 30)
print(t[-1] - t[0])
print(len(t[1:]))

x, y = 7, 3
x, y = y, x
print(x, y)

single = (9)
print(type(single))

3. 下面的代码想把元组里的价格改成打折价,但会报错。不改变"用元组存原价"的前提,让它正确输出打折后的新元组:

prices = (100, 200, 300)
prices[0] = 80
print(prices)
点击查看答案
# 1
info = ("小明", 2008, "上海")
name, year, city = info
print(f"我叫{name}{year} 年出生,来自{city}")

第 2 题:

  • t[-1] - t[0]20(30 - 10)
  • len(t[1:])2(切片得 (20, 30)
  • print(x, y)3 7(交换成功)
  • type(single)<class 'int'>(没加逗号,(9) 只是数字 9)

第 3 题: 元组不可修改,转成列表改完再转回来:

prices = (100, 200, 300)
temp = list(prices)
temp[0] = 80
prices = tuple(temp)
print(prices) # (80, 200, 300)

注意这不是"修改了原元组",而是新建了一个元组让 prices 指过去——原来那个 (100, 200, 300) 自始至终没变过。

本章小结

  • 元组 = 不可修改的列表,用 () 创建;读取类操作(索引/切片/遍历/in/len)与列表完全一致
  • 单元素元组必须带逗号 (5,)(5) 只是数字——逗号才是元组的灵魂
  • 改内容的操作全部报错;数据定死用元组,会变用列表
  • 解包 x, y = point:多变量赋值、a, b = b, a 交换、for i, v in enumerate() 全是它
  • tuple() / list() 互转

下一节是本章重头戏:4.3 字典 dict