第4章 4.2 元组 tuple 与命名元组
🎯 开场:为什么需要"不能改"的列表?
上一章我们学会了列表,它就像一张随时能贴撕改的便签——想加内容 append,想删内容 pop,想改某条直接赋值。非常灵活对吧?
但你有没有遇到过这种糟心事:
# 你定义了一个坐标点列表
points = [(1, 2), (3, 4), (5, 6)]
# 你的同事(或某个函数)不小心把它改了
points[0] = (100, 200)
# 更可怕的是,这种bug很难追查
print(points) # [(100, 200), (3, 4), (5, 6)] - 什么时候改的??
痛点来了:列表太"随和"了,谁都能改它。有时候你明明不想让某些数据被修改,偏偏拦不住。
这节课我们要学的元组(tuple),就像一张刻在石头上的便签——一旦写好,死都不能改。
学完这章,你能:
- 搞懂"不可变"是什么意思、为什么重要
- 用元组安全地存储不该被改的数据
- 用命名元组让代码更可读
- 避开新手必踩的 5 个坑
🧱 基础:元组核心概念
1. 什么是元组?
生活类比:元组就像结婚证——一旦领证,证上的信息就不能随便改了(要改得走正经程序)。你不会今天把结婚日期改成昨天,明天又改成明天对吧?
为什么要用:保护那些不该被改的数据,比如一年的月份名称、RGB 颜色值、数据库连接配置。
2. 创建元组
# 方式1:最常用,用小括号
rgb = (255, 128, 0)
# 方式2:省略括号(不推荐新手用)
point = 1, 2, 3
# 方式3:创建空元组
empty = ()
# 方式4:只有一个元素时,必须加逗号
single = (42,) # 不是 (42),那是int
print(rgb) # (255, 128, 0)
print(point) # (1, 2, 3)
print(empty) # ()
print(single) # (42,)
解释:第1行创建了一个 RGB 颜色元组;第11行注意了——单个元素的元组必须加逗号,否则 Python 把它当成普通括号表达式了。
3. 不可变特性——元组的核心
colors = ("红色", "绿色", "蓝色")
# ❌ 错误演示:尝试修改会报错
colors[0] = "黄色"
# TypeError: 'tuple' object does not support item assignment
# ✅ 正确做法:重新创建一个元组
colors = ("黄色", "绿色", "蓝色")
print(colors) # ('黄色', '绿色', '蓝色')
解释:元组创建后不能修改任何元素,想"改"只能重新创建一个。这看起来麻烦,其实是在保护你的数据。

4. 元组支持的操作(查、遍历、切片、拼接)
虽然不能改,但元组支持读取:
weekdays = ("周一", "周二", "周三", "周四", "周五", "周六", "周日")
# 1. 查询长度
print(len(weekdays)) # 7
# 2. 按索引查询
print(weekdays[0]) # 周一
print(weekdays[-1]) # 周日
# 3. 切片(和列表一样)
print(weekdays[0:3]) # ('周一', '周二', '周三')
# 4. 遍历
for day in weekdays:
print(f"今天是好日子:{day}")
# 5. 拼接(生成新元组,原来的不变)
weekend = ("周六", "周日")
workdays = ("周一", "周二", "周三", "周四", "周五")
all_days = workdays + weekend
print(all_days) # ('周一', '周二', '周三', '周四', '周五', '周六', '周日')
解释:切片和拼接都不会修改原元组,而是返回一个新元组。这保证了原数据的安全性。
5. 元组解包——超实用的特性
生活类比:解包就像拆快递——你收到一个"包含了3样东西的包裹",不用一件件拿出来,直接在签收单上签字,东西就自动摆到你面前了。
# 基本解包
rgb = (255, 128, 0)
red, green, blue = rgb
print(red, green, blue) # 255 128 0
# 交换两个变量(不需要临时变量!)
a = 1
b = 2
a, b = b, a
print(a, b) # 2 1
# 用 * 收集多余的元素
head, *middle, tail = (1, 2, 3, 4, 5)
print(head) # 1
print(middle) # [2, 3, 4]
print(tail) # 5
# 函数返回多个值(其实返回的就是元组)
def get_location():
return 116.404, 39.928 # 经度, 维度
lon, lat = get_location()
print(f"经度:{lon},纬度:{lat}")
解释:第1-2行把元组的每个元素"弹出来"赋给不同变量;第8-10行展示了元组解包最酷的用法——不用临时变量就能交换两个变量;第14行 *middle 把中间的元素收集成一个列表。
6. 命名元组(namedtuple)——让元组有名字
普通元组 point = (1, 2),你得记住 point[0] 是 x,point[1] 是 y。时间长了鬼记得住啊!
命名元组给你起名字的权利:
from collections import namedtuple
# 定义一个"点"类型
Point = namedtuple("Point", ["x", "y"])
# 创建实例
p = Point(10, 20)
# 用名字访问,清楚多了!
print(p.x, p.y) # 10 20
print(p[0], p[1]) # 10 20(仍然支持下标访问)
# 给字段起别名
p2 = Point(x=100, y=200)
print(p2.x, p2.y) # 100 200
解释:第3行定义了一个 Point 类型,它是一个特殊的元组,但每个位置都有名字。这样访问 p.x 比 p[0] 可读性强多了。

7. 命名元组的高级用法
from collections import namedtuple
# 定义学生信息
Student = namedtuple("Student", ["name", "age", "major", "gpa"])
# 创建学生
s1 = Student("小明", 20, "计算机", 3.8)
s2 = Student("小红", 22, "数学", 3.9)
# 1. 像字典一样访问(._asdict)
print(s1._asdict())
# {'name': '小明', 'age': 20, 'major': '计算机', 'gpa': 3.8}
# 2. 替换某些字段(._replace)—— 返回新元组,不改原数据
s3 = s1._replace(gpa=3.85, age=21)
print(s3)
# Student(name='小明', age=21, major='计算机', gpa=3.85)
# 3. 设置默认值
StudentWithDefault = namedtuple("StudentWithDefault",
["name", "age", "major", "gpa"])
StudentWithDefault.__new__.__defaults__ = ("匿名", 18, "未知", 0.0)
s4 = StudentWithDefault()
print(s4)
# StudentWithDefault(name='匿名', age=18, major='未知', gpa=0.0)
解释:命名元组虽然不可变,但用 _replace 可以创建"修改版"的新元组;__defaults__ 可以给某些字段设置默认值。
8. namedtuple vs dataclass 该用哪个?
| 特性 | namedtuple | dataclass |
|---|---|---|
| 可变性 | 不可变 | 可变(默认) |
| 代码量 | 少 | 更少 |
| 兼容性 | Python 2.7+ | Python 3.7+ |
| 适合场景 | 固定字段的"记录" | 需要经常修改的数据 |
# 如果你用 Python 3.7+,且需要可变字段,用 dataclass 更方便
from dataclasses import dataclass
@dataclass
class Student:
name: str
age: int
major: str
gpa: float
s = Student("小明", 20, "计算机", 3.8)
s.gpa = 3.9 # 可以直接改
print(s)
解释:dataclass 是 Python 3.7 引入的新类型,写起来更像类,但底层也是基于元组实现的。新手先用 namedtuple 够了,想深入再学 dataclass。
🔥 实战:3个递进项目
项目 1:坐标系统工具(5分钟)
场景:你需要存储一系列坐标点,并计算它们到原点的距离。
from collections import namedtuple
import math
# 定义坐标点类型
Point = namedtuple("Point", ["x", "y"])
# 创建几个坐标点
points = [
Point(3, 4), # 经典勾股定理例子
Point(6, 8),
Point(1, 1),
]
# 计算到原点距离的函数
def distance_to_origin(p):
return math.sqrt(p.x ** 2 + p.y ** 2)
# 计算并打印每个点到原点的距离
print("各点到原点的距离:")
for p in points:
dist = distance_to_origin(p)
print(f" 点 ({p.x}, {p.y}) -> 距离 {dist:.2f}")
# 找出离原点最远的点
farthest = max(points, key=distance_to_origin)
print(f"\n离原点最远的点:({farthest.x}, {farthest.y})")
预期输出:
各点到原点的距离:
点 (3, 4) -> 距离 5.00
点 (6, 8) -> 距离 10.00
点 (1, 1) -> 距离 1.41
离原点最远的点:(6, 8)
解释:用命名元组定义 Point,让代码比用普通列表 [x, y] 可读性强太多了。
项目 2:CSV 数据处理(15分钟)
场景:你有一个 CSV 文件存储了班级学生成绩,需要读取并计算平均分。
假设 students.csv 内容:
name,age,math,english
小明,18,85,92
小红,19,90,88
小刚,18,78,95
from collections import namedtuple
import csv
# 定义学生类型
Student = namedtuple("Student", ["name", "age", "math", "english"])
# 读取CSV文件
students = []
with open("students.csv", "r", encoding="utf-8") as f:
reader = csv.DictReader(f) # 读取成字典
for row in reader:
# 创建命名元组(注意类型转换)
s = Student(
name=row["name"],
age=int(row["age"]),
math=float(row["math"]),
english=float(row["english"])
)
students.append(s)
# 计算每个学生的平均分
print("学生成绩单:")
print("-" * 40)
for s in students:
avg = (s.math + s.english) / 2
print(f"{s.name} | 数学:{s.math:.0f} | 英语:{s.english:.0f} | 平均:{avg:.1f}")
# 计算班级平均分
class_avg_math = sum(s.math for s in students) / len(students)
class_avg_english = sum(s.english for s in students) / len(students)
print("-" * 40)
print(f"班级数学平均分:{class_avg_math:.1f}")
print(f"班级英语平均分:{class_avg_english:.1f}")
预期输出:
学生成绩单:
----------------------------------------
小明 | 数学:85 | 英语:92 | 平均:88.5
小红 | 数学:90 | 英语:88 | 平均:89.0
小刚 | 数学:78 | 英语:95 | 平均:86.5
----------------------------------------
班级数学平均分:84.3
班级英语平均分:91.7
解释:用 csv.DictReader 读取后转成命名元组,每条记录都有清晰的字段名,代码读起来像"流水账"一样自然。
项目 3:天气数据小工具(15分钟)
场景:你从 API 获取了一周的天气数据,需要统计并输出一个简洁的周报。
from collections import namedtuple
# 模拟API返回的天气数据(真实场景下你会用 requests 库)
api_response = {
"city": "北京",
"forecast": [
{"day": "周一", "temp": 25, "condition": "晴", "wind": 3},
{"day": "周二", "temp": 27, "condition": "多云", "wind": 2},
{"day": "周三", "temp": 23, "condition": "小雨", "wind": 5},
{"day": "周四", "temp": 22, "condition": "阴", "wind": 4},
{"day": "周五", "temp": 26, "condition": "晴", "wind": 2},
{"day": "周六", "temp": 28, "condition": "晴", "wind": 1},
{"day": "周日", "temp": 29, "condition": "多云", "wind": 3},
]
}
# 定义天气记录类型
Weather = namedtuple("Weather", ["day", "temp", "condition", "wind"])
# 解析数据
forecast = [
Weather(
day=d["day"],
temp=d["temp"],
condition=d["condition"],
wind=d["wind"]
)
for d in api_response["forecast"]
]
# 生成周报
print(f"📍 {api_response['city']} 一周天气周报")
print("=" * 50)
# 找最高/最低温
hottest = max(forecast, key=lambda w: w.temp)
coldest = min(forecast, key=lambda w: w.temp)
print(f"🌡️ 本周最高气温:{hottest.temp}°C({hottest.day} {hottest.condition})")
print(f"🌡️ 本周最低气温:{coldest.temp}°C({coldest.day} {coldest.condition})")
# 计算平均风力
avg_wind = sum(w.wind for w in forecast) / len(forecast)
print(f"💨 平均风力:{avg_wind:.1f} 级")
# 晴天统计
sunny_days = [w.day for w in forecast if w.condition == "晴"]
print(f"☀️ 晴天:{', '.join(sunny_days)}")
# 每日详情
print("\n📅 每日详情:")
print("-" * 50)
for w in forecast:
# 用 emoji 装饰天气状况
emoji = "☀️" if w.condition == "晴" else "⛅" if "多云" in w.condition else "🌧️"
print(f"{emoji} {w.day}: {w.condition}, {w.temp}°C, 风力{w.wind}级")
预期输出:
📍 北京 一周天气周报
==================================================
🌡️ 本周最高气温:29°C(周日 多云)
🌡️ 本周最低气温:22°C(周四 阴)
💨 平均风力:2.9 级
☀️ 晴天:周一, 周五, 周六
📅 每日详情:
--------------------------------------------------
☀️ 周一: 晴, 25°C, 风力3级
⛅ 周二: 多云, 27°C, 风力2级
🌧️ 周三: 小雨, 23°C, 风力5级
🌧️ 周四: 阴, 22°C, 风力4级
☀️ 周五: 晴, 26°C, 风力2级
☀️ 周六: 晴, 28°C, 风力1级
⛅ 周日: 多云, 29°C, 风力3级
解释:用命名元组处理结构化数据,代码既保持了不可变性保证数据安全,又比字典访问更清晰。配合列表推导式,代码简洁又易读。
💪 进阶:常见坑 + 调试技巧
坑 1:把元组当成可变参数传递
# ❌ 错误:以为修改了元组
def add_item(item, the_list):
the_list.append(item) # 元组没有 append!
my_tuple = (1, 2, 3)
add_item(4, my_tuple) # 报错:AttributeError
# ✅ 正确:返回新元组
def add_item_fixed(item, the_tuple):
return the_tuple + (item,)
my_tuple = (1, 2, 3)
my_tuple = add_item_fixed(4, my_tuple)
print(my_tuple) # (1, 2, 3, 4)
坑 2:混淆单个元素元组和普通括号
# ❌ 错误
single = (42) # 这是int,不是tuple
print(type(single)) # <class 'int'>
# ✅ 正确
single = (42,) # 逗号是关键
print(type(single)) # <class 'tuple'>
坑 3:在列表里存元组,但修改了元组内的可变对象
# ❌ 错误:元组本身不可变,但可以包含可变对象
weird = ([1, 2], [3, 4])
weird[0].append(99) # 不报错!因为修改的是列表,不是元组
print(weird) # ([1, 2, 99], [3, 4])
# ✅ 注意:这个坑很容易踩到,记住元组"不可变"指的是引用不可变
坑 4:元组解包时元素数量不匹配
# ❌ 错误
a, b = (1, 2, 3) # ValueError: too many values to unpack
# ✅ 正确:用 * 收集多余元素
a, b, *c = (1, 2, 3)
print(a, b, c) # 1 2 [3]
坑 5:namedtuple 字段名用关键字
# ❌ 错误
Point = namedtuple("Point", ["class", "def"]) # 报错:关键字不能做字段名
# ✅ 正确:换个名字,或者用 renamed 参数
Point = namedtuple("Point", ["class", "def"], rename=True)
p = Point(1, 2)
print(p._fields) # ('class', 'def') - 自动重命名了
性能小贴士:元组比列表省内存
import sys
# 元组比列表占用更少内存
list_demo = [1, 2, 3, 4, 5]
tuple_demo = (1, 2, 3, 4, 5)
print(sys.getsizeof(list_demo)) # 比 tuple 大
print(sys.getsizeof(tuple_demo)) # 更小
# 如果数据不需要修改,优先用元组
# Python 内部会对小元组做缓存,进一步省内存
a = (1, 2)
b = (1, 2)
print(a is b) # True - 同一个对象!
调试技巧:快速查看元组内容
# 技巧1:直接打印(适合jupyter和调试)
colors = (255, 128, 0)
print(f"调试:{colors}")
# 技巧2:用 _fields 查看命名元组字段
from collections import namedtuple
Point = namedtuple("Point", ["x", "y"])
print(Point._fields) # ('x', 'y')
# 技巧3:用 _asdict 转换成字典查看
p = Point(10, 20)
print(p._asdict()) # {'x': 10, 'y': 20}
✏️ 练习题
练习 1(2分钟):基本操作
- 输入:元组
(10, 20, 30, 40, 50) - 预期输出:
第二个元素是 20,最后一个元素是 50 - 提示:直接用索引访问,别想复杂了
练习 2(2分钟):解包操作
- 输入:元组
(1, 2, 3, 4, 5),用解包分离出首尾 - 预期输出:
首:1,尾:5 - 提示:可以用
first, *_, last = tuple
练习 3(3分钟):命名元组定义
- 输入:定义一个
Rect命名元组,包含width和height - 预期输出:创建一个
Rect(100, 50)并打印面积 - 提示:面积 = width × height
练习 4(5分钟):元组列表排序
- 输入:列表
[Point(3, 4), Point(1, 1), Point(5, 2)] - 预期输出:按 x 坐标从小到大排序输出
- 提示:用
sorted()+key参数
练习 5(5分钟):调试找错
- 题目:下面代码报错,请修复
from collections import namedtuple
Person = namedtuple("Person", "name age")
p = Person("小明", 18)
p.age = 19 # 想改年龄
- 预期输出:
Person(name='小明', age=19) - 提示:元组不可变,要用
_replace
作业:做一个「个人信息管理器」
需求描述:做一个命令行个人信息管理工具,用命名元组存储每个人的信息。
功能点:
1. 添加个人信息(姓名、年龄、职业、城市)
2. 查看所有人员列表
3. 按姓名查找某人
4. 按城市筛选人员
5. 删除某人
加分项:
1. 数据持久化(保存到文件,从文件加载)
2. 支持修改某人信息
验收标准:
- 能增删改查
- 代码有注释
- 运行不报错
提交方式:评论区贴代码或 GitHub 链接
📚 总结
本文学了 3 件事:
1. 元组是不可变的列表 —— 适合存不该改的数据
2. 解包是瑞士军刀 —— 交换变量、拆解数据、函数返回多值都靠它
3. 命名元组让数据有名字 —— 比索引访问可读性翻倍
延伸学习资源:
- 官方文档:https://docs.python.org/3/library/collections.html#collections.namedtuple
- 《Python编程:从入门到实践》第 3 章 - 数据结构
- 视频:B站小甲鱼《Python教程》第 38 讲
互动钩子:你在实际项目中用过元组吗?是用来保护数据还是做解包?欢迎评论区聊聊!用 namedtuple 做过什么有趣的东西?老粉优先回复哦 🚀

评论(0)