# 第 1-9 课:元组 ## 一、本课目标 完成本课后,你应该能够: 1. 解释元组与列表的主要区别; 2. 创建空元组、单元素元组和多元素元组; 3. 使用索引、负索引和切片读取元组; 4. 使用 `len()`、`in`、`index()` 和 `count()`; 5. 使用 `for` 遍历元组; 6. 理解元组不可变的含义; 7. 使用元组打包和解包多个数据; 8. 使用星号收集解包后的剩余元素; 9. 完成两个变量的交换; 10. 在列表和元组之间进行转换; 11. 判断什么场景更适合使用元组。 ## 二、前置知识 开始本课前,你应该已经掌握: - 列表的创建、索引和切片; - 列表的可变性; - 使用 `for` 遍历多个数据; - 使用 `in`、`index()` 和 `count()`; - 条件判断与 f-string。 ## 三、元组是什么 元组(Tuple)是一个有顺序的数据集合。 ```python agent_config = ("gpt-5", 0.7, 2000) ``` 元组与列表很相似: - 都能保存多个数据; - 都有顺序; - 都支持索引、负索引和切片; - 都能使用 `len()` 和 `for`。 最重要的区别是: - 列表是可变的,可以添加、删除和修改元素; - 元组是不可变的,创建后不能替换、增加或删除元素。 元组适合表达“创建后不希望被随意改变的一组数据”。 ## 四、创建元组 ### 4.1 多元素元组 ```python agent_config = ("gpt-5", 0.7, 2000) ``` 通常使用英文圆括号 `()` 包围元素,元素之间使用英文逗号分隔。 ### 4.2 空元组 ```python empty_tuple = () ``` 使用 `len(empty_tuple)` 会得到 `0`。 ### 4.3 单元素元组 单元素元组必须在元素后保留英文逗号: ```python single_tool = ("Python",) ``` 如果没有逗号: ```python not_a_tuple = ("Python") ``` Python 会把圆括号理解为普通分组,变量实际保存的是字符串。 可以使用 `type()` 验证: ```python print(type(("Python",))) # tuple print(type(("Python"))) # str ``` 创建元组时,真正决定元组的是逗号,而不只是圆括号。 ### 4.4 省略圆括号 ```python agent_position = "北京", "朝阳区" ``` 只要存在用于分隔元素的逗号,Python 也会创建元组。为了新手阅读清晰,本课大多数场景仍保留圆括号。 ## 五、元组可以保存不同类型 ```python agent_config = ("gpt-5", 0.7, 2000, True) ``` 这个元组依次保存: - 字符串:模型名称; - 浮点数:温度参数; - 整数:最大输出长度; - 布尔值:是否启用。 虽然允许混合类型,但每个位置应具有明确、稳定的含义。 ## 六、索引和负索引 元组索引从 `0` 开始: ```text 元素: gpt-5 0.7 2000 正索引: 0 1 2 负索引: -3 -2 -1 ``` ```python agent_config = ("gpt-5", 0.7, 2000) print(agent_config[0]) print(agent_config[-1]) ``` 访问不存在的索引会产生 `IndexError`。 访问首尾元素前,可以先判断元组是否非空: ```python if agent_config: print(agent_config[0]) print(agent_config[-1]) ``` ## 七、元组切片 切片规则与字符串、列表相同:包含开始索引,不包含结束索引。 ```python agent_config = ("gpt-5", 0.7, 2000, True) basic_config = agent_config[:2] remaining_config = agent_config[2:] reversed_config = agent_config[::-1] ``` 元组切片返回一个新元组,不会修改原元组。 ## 八、长度、成员判断和统计 ### 8.1 `len()` ```python config_count = len(agent_config) ``` ### 8.2 `in` ```python contains_model = "gpt-5" in agent_config ``` ### 8.3 `index()` ```python if "gpt-5" in agent_config: model_index = agent_config.index("gpt-5") ``` 元素不存在时,`index()` 会产生 `ValueError`,因此可以先使用 `in` 判断。 ### 8.4 `count()` ```python status_history = ("成功", "失败", "成功") success_count = status_history.count("成功") ``` ## 九、遍历元组 ```python supported_models = ("gpt-5", "o3", "gpt-4.1") for model in supported_models: print(model) ``` 需要编号时可以使用 `enumerate()`: ```python for number, model in enumerate(supported_models, start=1): print(f"{number}. {model}") ``` ## 十、元组不可变 下面的操作都不允许: ```text agent_config[0] = "o3" agent_config.append("新数据") del agent_config[0] ``` 元组没有 `append()`、`remove()`、`pop()` 等修改方法。 不可变带来的好处包括: - 表达数据结构是固定的; - 减少意外修改; - 阅读代码时更容易判断数据不会被增删; - 某些场景中元组可以作为字典的键,后续学习字典时会再讲。 ## 十一、需要“修改”元组怎么办 不能直接修改原元组,但可以创建新元组: ```python old_config = ("gpt-5", 0.7, 2000) new_config = ("o3",) + old_config[1:] print(old_config) print(new_config) ``` 原元组保持不变,`new_config` 是全新的元组。 也可以先转换为列表,修改后再转回元组: ```python config_list = list(old_config) config_list[0] = "o3" new_config = tuple(config_list) ``` 如果数据经常需要修改,直接使用列表通常更合适,不应反复转换。 ## 十二、元组拼接和重复 虽然不能修改原元组,但可以通过运算创建新元组。 ### 12.1 拼接 ```python basic_config = ("gpt-5", 0.7) limit_config = (2000, True) full_config = basic_config + limit_config ``` ### 12.2 重复 ```python default_status = ("待处理",) * 3 ``` 结果为: ```text ('待处理', '待处理', '待处理') ``` 这两种操作都返回新元组。 ## 十三、元组打包 把多个数据放进一个元组,称为打包(Packing): ```python agent_snapshot = "gpt-5", 0.7, 2000 ``` 三个数据被打包成一个元组。 使用圆括号可以让含义更清楚: ```python agent_snapshot = ("gpt-5", 0.7, 2000) ``` ## 十四、元组解包 把元组中的元素分别赋给多个变量,称为解包(Unpacking): ```python agent_snapshot = ("gpt-5", 0.7, 2000) model_name, temperature, max_tokens = agent_snapshot ``` 解包后: - `model_name` 保存 `"gpt-5"`; - `temperature` 保存 `0.7`; - `max_tokens` 保存 `2000`。 左侧变量数量必须与右侧元素数量一致,否则会产生 `ValueError`。 ## 十五、使用星号收集剩余元素 ```python agent_snapshot = ("gpt-5", 0.7, 2000, True) model_name, *other_settings = agent_snapshot ``` 解包后: - `model_name` 是字符串 `"gpt-5"`; - `other_settings` 是列表 `[0.7, 2000, True]`。 星号变量会把剩余元素收集到列表中。 一个解包表达式中只能有一个星号变量: ```python first_value, *middle_values, last_value = agent_snapshot ``` ## 十六、交换变量 传统思路通常需要临时变量。Python 可以使用打包和解包直接交换: ```python primary_model = "gpt-5" backup_model = "o3" primary_model, backup_model = backup_model, primary_model ``` 右侧先打包为元组,再解包给左侧变量。 ## 十七、元组内部包含可变对象 请观察: ```python agent_data = ("gpt-5", ["搜索", "计算"]) agent_data[1].append("写作") ``` 元组本身仍然不可变:不能把 `agent_data[1]` 替换成另一个列表。 但是第二个元素指向的列表本身是可变对象,因此列表内部可以改变。 这说明元组不可变的准确含义是:元组保存的元素引用不能被替换,并不保证元素内部的数据一定不可变。 这是一个重要边界。本课能看懂即可,后续学习对象和引用时会继续加深理解。 ## 十八、列表与元组如何选择 适合使用列表: - 数据需要经常添加、删除、修改; - 数量会变化; - 表达任务清单、工具集合、学习记录。 适合使用元组: - 数据结构固定; - 创建后不希望改变; - 表达坐标、颜色值、固定配置、数据库返回的一条固定结构记录; - 需要通过解包一次取得多个数据。 不要因为元组“看起来更高级”而强行使用。应该根据数据是否需要变化来选择。 ## 十九、运行本课示例 在 PowerShell 中执行: ```powershell cd D:\Code\Python\01_python基础\1_9_元组 python tuples.py ``` 示例无需用户输入,会依次展示创建、读取、切片、统计、遍历、拼接、转换、打包、解包和变量交换。 ## 二十、常见错误 ### 20.1 单元素元组忘记逗号 ```python single_value = ("Python",) ``` 最后的逗号不可省略。 ### 20.2 尝试直接修改元组 元组不支持按索引赋值,也没有列表的增删方法。 ### 20.3 解包变量数量不匹配 三个元素通常需要三个接收变量,除非使用星号收集剩余元素。 ### 20.4 认为圆括号一定代表元组 `("Python")` 是字符串;逗号才是创建单元素元组的关键。 ### 20.5 把元组中的列表也当作完全不可变 元组不能替换元素,但元组内部的列表仍然可以修改自身内容。 ## 二十一、课堂练习 打开 `practice.py`,完成“Agent 固定配置快照”。 运行命令: ```powershell python practice.py ``` 练习会要求你创建配置元组、读取和切片、查找统计、解包、转换以及比较原配置和新配置。 ## 二十二、思考题 1. 列表和元组最重要的区别是什么? 2. 为什么单元素元组必须保留逗号? 3. 元组切片会返回列表还是元组? 4. 解包时变量数量不匹配会发生什么? 5. 星号变量收集剩余元素后得到什么类型? 6. 为什么经常变化的数据更适合使用列表? 7. 元组中包含列表时,为什么列表内容仍然能够变化? ## 二十三、本课小结 - 元组是有顺序、不可变的数据集合; - 单元素元组必须包含逗号; - 元组支持索引、切片、遍历、成员判断和统计; - 元组不能直接增删或替换元素; - 需要新数据时,可以创建新元组或进行列表转换; - 打包把多个数据组合成元组; - 解包把元组元素分别赋给变量; - 星号可以收集剩余元素; - Python 可以通过打包解包交换变量; - 固定结构适合元组,经常变化的数据适合列表。 ## 二十四、验收标准 - [ ] 能创建空元组、单元素元组和多元素元组; - [ ] 能使用索引和切片读取元组; - [ ] 能使用 `len()`、`in`、`index()` 和 `count()`; - [ ] 能遍历元组; - [ ] 能解释元组不可变; - [ ] 能完成打包和解包; - [ ] 能使用星号收集剩余元素; - [ ] 能在列表和元组之间转换; - [ ] 能完成并运行 `practice.py`; - [ ] 能回答七道思考题。