元组(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。此外,zipenumerate、函数多返回值等 Python 特性底层都依赖元组。

四、命名元组(namedtuple)——给元组元素起名字

是什么collections.namedtuple 是元组的升级版——它和普通元组一样轻量、不可变,但你可以给每个位置起一个有意义的名字,通过 .属性名 而不仅仅是 [索引] 来访问元素。这让代码的可读性大幅提升——person.nameperson[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)。整个过程不需要临时变量,也没有修改底层内存——只是元组打包和解包的组合应用。