元组(Tuple):不可变序列、解包
一句话概述
元组(Tuple)是 Python 中"穿上盔甲的列表"——和列表一样有序、可索引、可切片,但一旦创建就不能修改(不可变)。这种不可变性带来三大好处:更安全(不会意外修改数据)、更快(内存更省、访问更快)、可哈希(能做字典的键和集合的元素)。元组还支持优雅的"解包"语法——一行代码把元组里的元素拆开赋给多个变量,这是 Python 最让人爱不释手的特性之一。很多你以为返回多个值的地方,幕后都是元组在默默工作。
💡 核心要点:①元组是不可变序列,用圆括号()创建,不支持增删改 ②元组比列表更轻量、更安全,适合存储不应被修改的数据 ③解包语法a, b = tup一行拆开赋值,配合*收集剩余元素 ④单元素元组必须加逗号(x, )否则被当作普通括号 ⑤函数返回多个值时实际上返回的是一个元组
教学与演示
一、元组的创建与本质——穿上盔甲的列表
是什么:元组(tuple)是 Python 内置的不可变序列类型。用圆括号 () 创建(也可以省略括号,直接用逗号分隔),元素之间用逗号分隔。元组一旦创建,就不能修改其内容——不能添加、删除或替换元素。它和列表共享索引和切片的语法,但不支持 append、remove 等修改操作。
大白话 列表是白板——你可以随时写写擦擦,修改内容。元组是刻在石头上的碑文——刻好了就不能改,只能读。为什么需要不能改的东西?因为有些数据天生就不应该被意外修改——比如一年 12 个月、一周 7 天、RGB 三原色的值。用元组存储这些数据,等于加了层"防弹衣"——谁不小心试图修改它,Python 直接报错提醒。
为什么:不可变性是元组存在的核心价值。它能防止意外的数据修改(这在多人协作的大型项目中极其重要),内存占用比列表更小(因为不需要预留扩展空间),访问速度略快于列表。最关键的是——不可变对象是"可哈希"的,这意味着元组可以作为字典的键(key)和集合(set)的元素,而列表做不到。在 AI 中,模型超参数配置、数据维度信息、颜色通道等通常用元组表示。
大白话 就像你的身份证号不该被修改一样,有些数据结构也不该被修改。用列表存坐标[x, y],万一某个函数不小心改了值,整个计算结果就错了。用元组(x, y),任何试图修改的操作都会立刻报错,错误在源头就被抓住了——这是"fail fast"的编程哲学。
怎么做:
import numpy as np
# ====== 1. 创建元组的多种方式 ======
# 方式一:用圆括号(最直观)
t1 = (1, 2, 3, 4, 5) # 整数元组
print("t1:", t1) # (1, 2, 3, 4, 5)
# 方式二:省略括号,直接用逗号(也合法!)
t2 = 1, 2, 3 # Python 自动识别为元组
print("t2:", t2, "类型:", type(t2).__name__) # (1, 2, 3) 类型: tuple
# 方式三:tuple() 构造函数,从可迭代对象转换
t3 = tuple("hello") # 从字符串创建字符元组
print("t3:", t3) # ('h', 'e', 'l', 'l', 'o')
t4 = tuple(range(5)) # 从 range 创建
print("t4:", t4) # (0, 1, 2, 3, 4)
t5 = tuple([1, 2, 3]) # 从列表转换(锁住列表!)
print("t5:", t5) # (1, 2, 3)
# ====== 2. 单元素元组——必须加逗号!(常见陷阱) ======
not_a_tuple = (42) # ❌ 这不是元组!括号被当作数学括号
print("(42) 的类型:", type(not_a_tuple).__name__) # int!
real_tuple = (42,) # ✅ 这才是单元素元组——逗号是关键
print("(42,) 的类型:", type(real_tuple).__name__) # tuple
also_tuple = 42, # ✅ 不加括号,逗号也能创建
print("42, 的类型:", type(also_tuple).__name__) # tuple
# ====== 3. 元组的不可变性——修改会报错 ======
t = (10, 20, 30)
print("\n原始元组:", t)
# 以下操作都会报错(已注释):
# t[0] = 999 ❌ TypeError: 'tuple' object does not support item assignment
# t.append(40) ❌ AttributeError: 'tuple' object has no attribute 'append'
# t.pop() ❌ AttributeError: 'tuple' object has no attribute 'pop'
# 但可以"间接修改"——如果元组里包含可变对象
t_with_list = (1, [2, 3], 4) # 元组第 1 个元素是列表(可变)
print("\n含列表的元组:", t_with_list)
t_with_list[1][0] = 999 # 元组本身没变,但里面的列表变了!
print("修改后:", t_with_list) # (1, [999, 3], 4)
# 注意:元组的不可变指的是引用不可变,不是被引用对象不可变
# ====== 4. 元组的索引与切片(和列表完全一样) ======
info = ("张三", 25, "北京", "工程师")
print("\n=== 元组的索引和切片 ===")
print("info[0] =", info[0]) # '张三' — 索引访问
print("info[-1] =", info[-1]) # '工程师' — 负数索引
print("info[1:3]=", info[1:3]) # (25, '北京') — 切片返回新元组
# ====== 5. 元组的常用内置操作 ======
nums = (5, 2, 8, 2, 5, 2)
print("\n=== 元组常用操作 ===")
print("len():", len(nums)) # 6 — 元素个数
print("count(2):", nums.count(2)) # 3 — 统计 2 出现的次数
print("index(8):", nums.index(8)) # 2 — 查找 8 首次出现的位置
print("max():", max(nums)) # 8 — 最大值
print("min():", min(nums)) # 2 — 最小值
print("sum():", sum(nums)) # 24 — 求和
print("5 in nums:", 5 in nums) # True — 成员检查
print("sorted:", sorted(nums)) # [2, 2, 2, 5, 5, 8] — 排序返回列表
# ====== 6. 元组拼接与重复 ======
a = (1, 2)
b = (3, 4)
print("\n拼接 a + b:", a + b) # (1, 2, 3, 4) — 拼接产生新元组
print("重复 a * 3:", a * 3) # (1, 2, 1, 2, 1, 2) — 重复产生新元组
# 注意:拼接和重复都是创建新元组,原元组不变!什么用:在 AI 中,元组的不可变特性非常契合"配置数据"的场景。模型的输入维度 input_shape = (224, 224, 3)(高、宽、通道)、训练超参数 hyperparams = (0.001, 32, 100)(学习率、batch大小、epoch数)、颜色值 RED = (255, 0, 0)——这些都不应该被中途修改,用元组是最佳选择。TensorFlow/PyTorch 中大量使用元组来表示张量的形状(shape)。
二、解包——Python 最优雅的多重赋值
是什么:元组解包(Unpacking)是将元组(或任何可迭代对象)中的元素一次性拆分并赋值给多个变量的语法。基本形式是 a, b, c = (1, 2, 3),效果等同于 a=1; b=2; c=3,但一行搞定。解包也支持用 * 前缀收集剩余元素到一个列表中。
大白话 解包就像拆快递——你收到一个包裹(元组),里面有书、衣服、零食。不用拆开袋子一件件掏——直接书, 衣服, 零食 = 包裹,三样东西就各自到了自己的变量里。如果包裹里的东西数量不确定,还可以用*说"剩下的全给我装这个袋子里"——a, *rest, b = (1, 2, 3, 4, 5)让 a=1, b=5, rest=[2,3,4]。
为什么:解包是 Python 最具"Python 内味"的语法之一。它极大地简化了多重赋值、变量交换、函数返回多值等常见场景。在 C/Java 中交换两个变量需要三行代码和一个临时变量,Python 一行 a, b = b, a 就搞定。解包让代码更简洁、更可读、更不容易出错——你不需要手动索引元组的每个位置。
大白话 没有解包的世界:函数返回(x, y, z),你得result = func(); x = result[0]; y = result[1]; z = result[2]——三行丑陋的代码。有解包:x, y, z = func()——一行优雅的 Python。这不是语法糖,这是生产力。
怎么做:
import numpy as np
# ====== 1. 基本解包——一对一赋值 ======
# 最常见的形式:等号左边变量数和右边元素数相等
point = (3, 7) # 表示坐标 (x, y)
x, y = point # 解包:x=3, y=7
print(f"x={x}, y={y}") # x=3, y=7
# 可以省略括号
name, age, city = "Alice", 30, "上海" # 右边自动打包成元组,然后解包
print(f"{name}, {age}岁, {city}")
# ====== 2. 星号 * 解包——收集剩余元素 ======
# *变量名:把剩余的元素收集到一个列表中
numbers = (1, 2, 3, 4, 5)
first, *middle, last = numbers # 1→first, [2,3,4]→middle, 5→last
print(f"first={first}, middle={middle}, last={last}")
# * 可以在任意位置
*head, tail = numbers # 除了最后一个,全给 head
print(f"head={head}, tail={tail}") # head=[1,2,3,4], tail=5
head, *tail = numbers # 除了第一个,全给 tail
print(f"head={head}, tail={tail}") # head=1, tail=[2,3,4,5]
# ====== 3. 解包的经典应用:一行交换变量 ======
a, b = 10, 20
print(f"\n交换前: a={a}, b={b}")
a, b = b, a # 背后:右边打包(b,a)→元组,再解包给左边
print(f"交换后: a={a}, b={b}") # a=20, b=10
# 三变量同时交换
x, y, z = 1, 2, 3
x, y, z = z, x, y # 循环右移
print(f"循环交换: x={x}, y={y}, z={z}") # x=3, y=1, z=2
# ====== 4. 遍历中的解包——枚举 (enumerate) ======
fruits = [("苹果", 5), ("香蕉", 3), ("橘子", 8)] # 列表中每个元素是元组
print("\n=== 遍历中解包 ===")
for name, price in fruits: # 每次循环直接解包成 name 和 price
total = price * 1.1 # 加 10% 税
print(f"{name}: 单价{price}元, 含税{total:.1f}元")
# enumerate 返回 (索引, 元素) 元组,直接解包
print()
for i, fruit in enumerate(["猫", "狗", "鸟"]):
print(f"第{i}个: {fruit}")
# zip 并行遍历,解包
names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]
print()
for name, score in zip(names, scores): # zip 返回 (name, score) 元组
print(f"{name}: {score}分")
# ====== 5. 函数返回多值(幕后是元组) ======
def min_max_avg(data):
"""计算最小值、最大值、平均值——返回三个值"""
return min(data), max(data), sum(data)/len(data) # 逗号分隔=返回元组
data = [5, 2, 9, 1, 7]
mi, ma, avg = min_max_avg(data) # 解包接收三个返回值
print(f"\n数据: {data}")
print(f"最小值={mi}, 最大值={ma}, 平均值={avg:.2f}")
# 验证:函数确实返回了元组
result = min_max_avg(data)
print(f"返回类型: {type(result).__name__}") # tuple
print(f"返回值: {result}") # (1, 9, 5.8)
# ====== 6. AI 实战:解包数据 batch ======
# 模拟:每个训练样本是 (特征向量, 标签) 的元组
samples = [
([0.2, 0.8, 0.5], 1), # 特征 [x1, x2, x3],标签 1
([0.7, 0.1, 0.3], 0), # 特征 [x1, x2, x3],标签 0
([0.4, 0.6, 0.9], 1),
]
print("\n=== 模拟训练遍历 ===")
for i, (features, label) in enumerate(samples): # 双重解包!
# features 是特征列表,label 是标签
pred = np.mean(features) # 简单用均值模拟预测值
correct = (pred > 0.5) == bool(label) # 模拟判断预测是否正确
print(f"样本{i}: 特征{features}, 标签{label}, 预测均值={pred:.2f}, {'✓' if correct else '✗'}")什么用:解包在 AI 代码中几乎无处不在。遍历 DataLoader 的 batch 时:for images, labels in dataloader:(每个 batch 是一个元组);enumerate() 获取 epoch 计数:for epoch, (X, y) in enumerate(train_loader):;模型输出解包:logits, hidden = model(x);zip() 并行处理多个数据源。这些看似理所当然的写法,背后都是元组解包在支撑。
三、元组与列表的对比——什么时候用哪个?
是什么:元组和列表是 Python 中最相似也是最容易被混淆的两种序列类型。它们的核心区别在于可变性——列表可变,元组不可变。这个区别决定了两者在内存管理、使用场景、安全性上的本质不同。
大白话 列表是租房——你可以重新粉刷墙壁、换家具、甚至砸掉隔墙(增删改)。元组是买房——交房后格局就定死了,不能改结构,只能"看看"(读)。租房灵活,适合临时居住(动态数据);买房安全,适合长期定居(固定配置)。
为什么:选错数据结构可能导致两种后果——选了列表存不该改的数据,被意外修改导致 bug(安全性问题);选了元组存需要动态变化的数据,天天 list() 转换浪费性能(效率问题)。理解两者的适用场景,是写出稳健 Python 代码的基本功。
大白话 一个简单判断标准:如果数据的"身份"(是什么)比"内容"(值是多少)更重要,用元组。比如坐标 (x, y)——(3, 5) 和 (5, 3) 是两个完全不同的坐标。如果数据的"内容"在运行时需要变化,用列表。比如购物车——商品会加加减减。
怎么做:
import numpy as np
# ====== 1. 对比表格——一张图看懂区别 ======
# 列表:可变,用 [] 创建
lst = [1, 2, 3]
lst[0] = 999 # ✅ 列表可以修改
lst.append(4) # ✅ 列表可以追加
print("列表修改后:", lst) # [999, 2, 3, 4]
# 元组:不可变,用 () 创建
tup = (1, 2, 3)
# tup[0] = 999 ❌ 不能修改
# tup.append(4) ❌ 没有 append 方法
print("元组:", tup) # (1, 2, 3)
# ====== 2. 内存占用对比 ======
# 元组比列表更省内存
import sys
lst_small = [1, 2, 3]
tup_small = (1, 2, 3)
print(f"\n列表内存: {sys.getsizeof(lst_small)} 字节") # 通常更大
print(f"元组内存: {sys.getsizeof(tup_small)} 字节") # 通常更小
# 原因:列表需要预留额外空间以支持动态扩容
# ====== 3. 可哈希性——元组能做字典键,列表不行 ======
# 元组可以做字典的键
locations = {
(39.9, 116.4): "北京", # 经纬度元组作键
(31.2, 121.5): "上海",
(22.5, 114.1): "深圳",
}
print(f"\n(39.9, 116.4) 是: {locations[(39.9, 116.4)]}") # 北京
# 列表不能做字典键
# d = {[1, 2]: "value"} ❌ TypeError: unhashable type: 'list'
# ====== 4. 使用场景决策指南 ======
# 【场景一】数据在程序运行中不变 → 元组
DAYS_OF_WEEK = ("周一", "周二", "周三", "周四", "周五", "周六", "周日")
RGB_WHITE = (255, 255, 255)
# 【场景二】数据需要增删改 → 列表
shopping_cart = ["牛奶", "面包"] # 动态购物车
# 【场景三】函数返回多个值 → 元组(Python 默认行为)
def get_model_config():
return "ResNet", 50, (224, 224, 3) # 返回 (模型名, 层数, 输入尺寸)
model_name, layers, input_size = get_model_config()
print(f"\n模型: {model_name}, 层数: {layers}, 输入: {input_size}")
# 【场景四】需要可哈希的键 → 元组
cache = {} # 用元组做缓存键
for color in [(255, 0, 0), (0, 255, 0), (0, 0, 255)]:
key = color # 元组作为键
cache[key] = f"颜色{color}已缓存"
print(f"\n缓存键: {list(cache.keys())}")
# ====== 5. 互相转换 ======
my_list = [1, 2, 3]
my_tuple = tuple(my_list) # 列表 → 元组:锁住数据!
print(f"\n列表→元组: {my_tuple}, 类型: {type(my_tuple).__name__}")
my_tuple2 = (4, 5, 6)
my_list2 = list(my_tuple2) # 元组 → 列表:解锁数据!
print(f"元组→列表: {my_list2}, 类型: {type(my_list2).__name__}")
# AI 中常见:配置用元组,处理时转列表
config_shape = (28, 28, 1) # 配置用不可变元组
# ... 后续处理时 ...
shape_list = list(config_shape) # 需要修改时转列表
shape_list[2] = 3 # 修改通道数
print(f"修改后的 shape: {tuple(shape_list)}") # 再转回元组保存什么用:在 AI 工程中,tuple 主要用于"配置类数据"——模型输入 shape、超参数组合、颜色空间定义。list 则用于"运行态数据"——训练损失记录、batch 数据、预测结果。实际开发中,遵循"默认用元组,需要修改时转列表"的原则可以减少很多意外修改的 bug。此外,zip、enumerate、函数多返回值等 Python 特性底层都依赖元组。
四、命名元组(namedtuple)——给元组元素起名字
是什么:collections.namedtuple 是元组的升级版——它和普通元组一样轻量、不可变,但你可以给每个位置起一个有意义的名字,通过 .属性名 而不仅仅是 [索引] 来访问元素。这让代码的可读性大幅提升——person.name 比 person[0] 清晰得多。
大白话 普通元组就像没有标签的行李箱——(180, 75, 25)你知道是身高、体重、年龄,但别人看不懂。命名元组给每个位置贴了标签——.身高、.体重、.年龄——任何人一眼就知道意思,而且还保留了元组的轻量和不可变性。
为什么:当元组元素超过 2-3 个时,靠位置索引访问很快变得难以维护——你总会忘记第 0 位是啥、第 3 位是啥。命名元组解决了这个问题,同时内存开销和普通元组几乎一样(没有传统类的 __dict__ 开销)。在 AI 中,当需要传递结构化数据(如数据样本、超参数、评估指标)但不想引入完整类的复杂度时,namedtuple 是最佳选择。
怎么做:
import numpy as np
from collections import namedtuple
# ====== 1. 创建命名元组类 ======
# 语法:namedtuple('类名', '字段1 字段2 字段3')
Person = namedtuple('Person', 'name age city') # 创建命名元组"工厂"
# 也可以用列表:namedtuple('Person', ['name', 'age', 'city'])
# ====== 2. 创建命名元组实例 ======
p1 = Person('张三', 25, '北京') # 按位置传参
p2 = Person(name='李四', age=30, city='上海') # 按关键字传参(更清晰)
print("p1:", p1) # Person(name='张三', age=25, city='北京')
print("p2:", p2)
# ====== 3. 通过名字访问(比 [0]、[1] 清晰多了!) ======
print("\n=== 命名访问 vs 索引访问 ===")
print(f"姓名(命名): {p1.name}") # 张三 — 清晰直观!
print(f"姓名(索引): {p1[0]}") # 张三 — 也能用,但不直观
print(f"年龄: {p1.age}, 城市: {p1.city}")
# ====== 4. 命名元组仍然是元组——不可变、可解包 ======
# p1.name = '王五' ❌ AttributeError: can't set attribute
# 但可以解包
name, age, city = p1
print(f"\n解包: {name}, {age}, {city}")
# 也支持索引和切片
print(f"p1[:2]: {p1[:2]}") # ('张三', 25)
# ====== 5. 常用方法:_replace, _asdict ======
# _replace:返回一个新命名元组,替换指定字段(原变量不变!)
p3 = p1._replace(age=26) # 创建新的,age 改成 26
print(f"\n_replace 后: {p3}") # Person(name='张三', age=26, city='北京')
print(f"原 p1 没变: {p1}") # 确认原变量不变
# _asdict:转成字典
print(f"_asdict: {p1._asdict()}") # {'name': '张三', 'age': 25, 'city': '北京'}
# ====== 6. AI 实战:用命名元组管理训练样本 ======
# 定义样本结构
Sample = namedtuple('Sample', 'features label weight')
# 创建一批模拟样本
samples = [
Sample(np.array([0.1, 0.8, 0.3]), 1, 1.0), # 特征, 标签, 样本权重
Sample(np.array([0.7, 0.2, 0.5]), 0, 0.8),
Sample(np.array([0.4, 0.6, 0.9]), 1, 1.0),
]
print("\n=== 训练样本遍历 ===")
# 遍历时直接用名字访问,代码自解释!
for i, s in enumerate(samples):
confidence = np.mean(s.features) # s.features 比 s[0] 清晰多了
weighted = confidence * s.weight # s.weight 一目了然
print(f"样本{i}: 特征均值={confidence:.2f}, 标签={s.label}, 权重={s.weight}, 加权={weighted:.2f}")
# ====== 7. 命名元组用于配置 ======
# AI 模型训练配置
TrainConfig = namedtuple('TrainConfig',
'learning_rate batch_size epochs dropout input_dim')
config = TrainConfig(
learning_rate=0.001,
batch_size=32,
epochs=100,
dropout=0.5,
input_dim=784
)
print(f"\n训练配置:")
print(f" 学习率: {config.learning_rate}")
print(f" 批大小: {config.batch_size}")
print(f" 训练轮: {config.epochs}")
print(f" Dropout: {config.dropout}")
print(f" 输入维度: {config.input_dim}")什么用:在 AI 项目中,namedtuple 非常适合替代轻量级的配置类和数据容器。TensorFlow 的 tf.data.Dataset 内部就用类似 namedtuple 的机制表示样本。PyTorch 的 torch.return_types 也是类似的设计。使用 namedtuple 而不是普通类,既节省了内存(开销远小于普通类),又保持了代码的可读性。特别是在处理大量小对象时(如百万级样本),namedtuple 的内存优势非常明显。
概念关系图谱
| 概念 | 核心含义 | 与AI的关系 | 关联概念 |
|---|---|---|---|
| tuple | 不可变序列,创建后不能增删改 | 模型配置、输入shape、超参数 | list、namedtuple |
| 不可变性 | 对象创建后状态不可改变 | 防止意外修改配置,确保数据安全 | 可哈希、字符串 |
| 解包 | 一行拆开赋值给多个变量 | for循环遍历batch、函数多返回值 | *星号解包、zip |
| namedtuple | 有字段名的轻量级元组 | 样本结构定义、训练配置管理 | 元组、类 |
| _replace | 返回替换字段后的新命名元组 | 动态调整配置参数 | 不可变性 |
| 可哈希 | 可作为字典键、集合元素 | 缓存键、去重 | 不可变、hash() |
| zip | 并行迭代多个可迭代对象 | 并行处理特征和标签 | 解包、enumerate |
重点答疑
Q1: 元组和列表的核心区别是什么?什么时候用哪个?
核心区别是可变性:元组不可变(创建后不能增删改),列表可变。元组更安全、更省内存、可哈希(能当字典键);列表更灵活、支持动态增删。简单判断:数据在程序运行中不会改变 → 用元组(如配置、常量、坐标);数据需要增删改或动态构建 → 用列表(如收集结果、构建数据集)。Python 社区习惯:默认用列表,除非有不修改的理由。
Q2: 单元素元组为什么必须加逗号?
因为括号 () 在 Python 中有双重身份——它既是元组的创建符号,又是数学表达式的分组符号。(42) 被解析器当作"括号括起来的整数 42",而不是元组。只有加上逗号 (42, ) 或 42,,Python 才知道你想要的是元组。这是初学者最容易踩的坑之一。记住:逗号决定元组,括号只是辅助。
Q3: 元组解包中 * 变量的原理是什么?
*variable 是 Python 3 引入的"扩展解包"(Extended Unpacking)。它告诉 Python:"除了明确分配的那些元素,剩下的全装进这个变量里"。被收集的元素一定放在列表中(不是元组)。* 可以用在任意位置:first, *middle, last = (1,2,3,4,5) 得到 middle = [2,3,4]。在 Python 的规范中,*variable 也被称为"星号表达式"(starred expression)。函数参数中的 *args 其实就是这个机制。
Q4: 元组真的比列表快吗?快在哪里?
是的,但差别通常很小(微秒级别)。元组快的两个原因:①内存分配更简单——元组不需要预留额外空间(列表通常会预留),所以创建元组比创建相同内容的列表快;②访问速度——由于元组的不可变性,Python 解释器可以对元组做更多优化(比如常量折叠)。但在实际开发中,这个性能差异远不如"选择正确的数据结构"带来的代码清晰度重要。
Q5: 函数的多个返回值到底是元组还是别的什么?
函数的多个返回值本质上就是一个元组。当你写 return a, b, c 时,Python 先把它打包成 (a, b, c) 再返回。调用方用 x, y, z = func() 接收时,Python 自动解包。你可以验证:result = func(); print(type(result)) 会输出 <class 'tuple'>。如果只用一个变量接收:result = func(),result 就是完整的元组。
章节单词汇总
| 英文 | 音标 | 术语/释义 |
|---|---|---|
| tuple | /tʌpl/ 或 /tuːpl/ | 元组;不可变序列 |
| immutable | /ɪˈmjuːtəbl/ | 不可变的;创建后不可修改 |
| unpacking | /ʌnˈpækɪŋ/ | 解包;拆分赋值给多个变量 |
| hashable | /ˈhæʃəbl/ | 可哈希的;可作为字典键 |
| namedtuple | /neɪmd ˈtʌpl/ | 命名元组;有字段名的轻量元组 |
| sequence | /ˈsiːkwəns/ | 序列;有序的数据结构 |
| starred | /stɑːrd/ | 星号的;指 *variable 语法 |
| lightweight | /ˈlaɪtweɪt/ | 轻量的;内存占用小 |
面试练习
Q1 [单选] 以下哪个不是元组?
- A.
(1, 2, 3) - B.
1, 2, 3 - C.
(42,) - D.
(42)
解答:D 不是元组——(42) 被当作整数 42 的括号表达式。A、B、C 都是元组(B 自动打包,C 有逗号)。判断是否为元组的关键是看有没有逗号。
Q2 [单选] t = (1, 2, 3); t[0] = 10 执行后会发生什么?
- A. t 变成
(10, 2, 3) - B. 抛出 TypeError
- C. t 变成
(1, 2, 3, 10) - D. 什么事都不发生
解答:B 正确。元组是不可变的,t[0] = 10试图修改元组元素会抛出TypeError: 'tuple' object does not support item assignment。
Q3 [单选] a, *b, c = (1, 2, 3, 4, 5) 执行后,b 的值是?
- A.
2 - B.
[2, 3, 4] - C.
(2, 3, 4) - D.
[1, 2, 3, 4]
解答:B 正确。a=1, c=5, 剩余的 2,3,4 被收集进列表 b=[2,3,4]。注意星号收集的结果始终是列表(list),不是元组。
Q4 [多选] 以下关于元组和列表的说法,正确的是?
- A. 元组可以作为字典的键,列表不行
- B. 元组占用的内存通常比相同元素的列表更少
- C. 元组不支持索引操作
- D.
tuple([1, 2, 3])可以把列表转成元组
解答:A、B、D 正确。C 错误——元组支持索引,和列表语法完全一样。D 正确,tuple() 可以将任意可迭代对象转为元组。
Q5 [单选] 以下代码的输出是什么?def f(): return 1, 2, 3; print(type(f()))
- A.
<class 'int'> - B.
<class 'tuple'> - C.
<class 'list'> - D. 报错
解答:B 正确。使用逗号分隔的多个返回值,Python 会自动打包成一个元组。所以f()返回(1, 2, 3),类型是 tuple。
Q6 [多选] 以下哪些方式可以创建合法的元组?
- A.
t = 1, 2, 3 - B.
t = (1,) - C.
t = tuple("abc") - D.
t = ()
解答:全部正确。A 不用括号自动打包;B 是单元素元组;C 从字符串转换得到 ('a', 'b', 'c');D 是空元组。
Q7 [单选] 以下哪个操作是合法且不会报错的?
- A.
(1, 2, 3).append(4) - B.
(1, 2, 3).count(2) - C.
(1, 2, 3).sort() - D.
(1, 2, 3).pop()
解答:B 合法——count()不修改元组,只统计。A、C、D 都试图修改元组,会报 AttributeError。元组的方法只有两个:count()和index()。
Q8 [单选] tup = (1, [2, 3], 4); tup[1].append(5); print(tup) 输出什么?
- A.
(1, [2, 3, 5], 4) - B.
(1, [2, 3], 4, 5) - C. 报错 TypeError
- D. 报错 AttributeError
解答:A 正确。元组的"不可变"指的是元组中每个位置指向的对象引用不可变——但引用指向的可变对象(列表)本身可以变。这里 tup[1] 指向的列表没有被替换引用,只是列表内容变了,所以不违反元组的不可变性。
Q9 [多选] 关于命名元组 namedtuple,以下说法正确的是?
- A. namedtuple 可以通过
.属性名访问元素 - B. namedtuple 的元素可以被修改
- C. namedtuple 比普通类的内存开销更小
- D.
_replace()方法返回一个新的命名元组
解答:A、C、D 正确。B 错误——namedtuple 继承自 tuple,仍然不可变。C 正确(没有__dict__属性字典)。D 正确,_replace返回新对象,原对象不变。
Q10 [单选] 交换两个变量 a, b = b, a 的原理是?
- A. Python 创建了一个临时变量来辅助交换
- B. Python 直接修改了内存地址
- C. 右边先打包成元组
(b, a),然后解包赋值给左边 - D. Python 使用了 XOR 交换算法
解答:C 正确。执行的顺序是:①右边b, a被求值为元组(如(20, 10));②左边a, b解包接收(a=20, b=10)。整个过程不需要临时变量,也没有修改底层内存——只是元组打包和解包的组合应用。