42  数据结构之元组

本章定位(复习与补充训练层):本章对应正课第2章《列表、字典和元组》(章节 2)的元组部分,并与第37章《数据基本结构(元组、列表、字典)》(章节 38)互为补充训练(本章为分项版),用于复习、补缺与额外练习。建议先不看讲解,直接尝试下方平台任务,再对照解析补弱项。本章不属于必修主线。先做本章『动手与思考』第 1 题与平台任务自测,通过即可跳过本章。

42.1 引言:元组的不可变性及其在数据科学中的意义

元组(Tuple)的核心特征是不可变性(Immutability)——通过限制修改来保证数据的完整与安全,这使它特别适合存储交易参数、历史行情等一经生成就不应改变的内容。本章用三个平台任务检验你对元组创建、索引切片与整体重建的复习效果。

42.2 本章学习目标

本章为复习与补充训练,目标是检验你是否能脱离正课独立完成下列任务:

  1. 能独立完成平台任务1:用 type() 逐一检查股票信息各变量(名称、代码、日期、交易所、总股本、流通股本、发行价、现价、涨跌幅)的数据类型
  2. 能独立完成平台任务2:把上述九个字段打包成元组 tup,并输出整个元组、首元素 tup[0]、末元素 tup[-1] 与切片 tup[2:6]
  3. 能独立完成平台任务3:用 del 删除整个元组变量,再把含 H 股价格与涨跌幅的十一个字段打包成新元组 tup_new 输出
  4. 能说出单元素元组必须带逗号、元组”不可变但可整体重建”、以及元组能作字典键而列表不能的原因

若上述任务无法独立完成,请回到正课第2章元组部分补弱,再回到本章练习。

42.3 元组的五大核心特性

元组之所以在Python生态系统中占据重要地位,源于其以下五个相互关联的核心特性:

1. 不可变性(Immutability)

不可变性是元组最本质的特征。这意味着一旦元组被创建,其内容不能被增加、删除或修改。这一特性看似是一种限制,实则是编程中的重要保障机制。从软件工程角度看,不可变性带来了:

  • 数据一致性: 消除了因意外修改导致的数据不一致风险
  • 代码可预测性: 调用函数时,传入的元组参数不会被函数内部操作改变
  • 内存共享安全: 多个变量可以安全地引用同一个元组,无需担心副作用

再从计算理论的角度看,不可变数据结构具有显著优势:

  1. 线程安全: 多线程环境下,不可变对象无需加锁即可安全访问
  2. 哈希能力: 只有不可变对象才能作为字典的键或集合的元素
  3. 内存优化: Python解释器可以对元组进行特定优化,减少内存占用
  4. 语义清晰: 不可变性明确传达了”只读”的编程意图

2. 有序性(Ordered)

元组中的元素按照插入顺序存储,可以通过索引(从0开始)访问。这一点与列表相同,但与集合、字典等无序结构形成对比。在金融时间序列分析中,顺序性至关重要——数据点的先后次序往往代表了时间先后,这种信息本身就有价值。

3. 异构性(Heterogeneous)

一个元组可以包含不同类型的元素:整数、浮点数、字符串、甚至其他元组。这种灵活性使得元组特别适合表示”记录”或”结构体”。例如,一个股票的tick数据可以表示为:

tick_data = ('600519.SH', '2024-01-15 09:30:00', 1856.00, 100)
# 分别代表:股票代码、时间戳、价格、成交量

4. 轻量性(Lightweight)

由于元组的不可变性,Python解释器可以对其进行特定优化。在内存占用方面,元组通常比列表更节省空间。在C语言实现中,元组对象的结构比列表简单,不需要维护动态扩容的额外开销。对于需要创建大量小对象的场景,使用元组可以显著降低内存压力。

5. 可迭代性(Iterable)

元组是可迭代对象,可以用于for循环、列表推导式等上下文。这使得元组可以无缝集成到Python的数据处理流程中,与其他数据结构配合使用。

42.4 元组的创建方法

Python提供了多种创建元组的方式,每种方式都有其适用场景和注意事项。理解这些方法的细微差别有助于编写更清晰、更高效的代码。

方法1:使用圆括号创建

最常见的方法是使用圆括号将元素括起来,元素之间用逗号分隔。

平台任务1(平台原始代码)

以下代码与教学平台任务要求完全一致:

任务要求:给定中国通号A股的九个字段变量(名称、代码、上市日期、交易所、总股本、流通股本、发行价、现价、涨跌幅),逐一用 type() 输出各变量的数据类型。请将代码原样输入教学平台(注释除外),判定以平台为准。

列表 42.1: 平台原始代码 (1)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
name = "中国通号"  # 设置名称为"中国通号"
print('name:',type(name))  # 输出name
 
code = "688009"  # 设置代码为"688009"
print('code:',type(code))  # 输出code
 
IPO_date = "2019年7月22日"  # 设置日期为"2019年7月22日"
print('IPO_data:',type(IPO_date))  # 输出IPO_data
 
exchange = "上海证券交易所"  #以字符串输入中国通号A股的上市交易所
print('exchange',type(exchange))               #使用type函数输出数据类型
 
capital = 10589819000  # 总股本:10589819000股
print('capital',type(capital))  # 输出capital
 
shares = 8621018000  # 流通股本:8621018000股
print('share:',type(shares))  # 输出share
 
price_IPO = 5.85          #以浮点型输入中国通号A股发行价
print('price_IPO:',type(price_IPO))    #使用type函数输出数据类型并使用print函数打印
 
price_Jul22 = 12.27  # 设置当前价格为12.27
print('price_Jul22:',type(price_Jul22))  # 输出price_Jul22
 
change_Jul22 = 1.09744  # 涨幅:109.744%
print('change_Jul22:',type(change_Jul22))  # 输出change_Jul22

预期输出(本机实际运行结果,具体以平台运行结果为准):

name: <class 'str'>
code: <class 'str'>
IPO_data: <class 'str'>
exchange <class 'str'>
capital <class 'int'>
share: <class 'int'>
price_IPO: <class 'float'>
price_Jul22: <class 'float'>
change_Jul22: <class 'float'>

平台任务2(平台原始代码)

以下代码与教学平台任务要求完全一致:

任务要求:把上述九个字段打包成元组 tup,输出整个元组,再分别访问并输出首元素 tup[0]、末元素 tup[-1] 与切片 tup[2:6]。请将代码原样输入教学平台(注释除外),判定以平台为准。

列表 42.2: 平台原始代码 (2)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
name = "中国通号"  # 设置名称为"中国通号"
code = "688009"  # 设置代码为"688009"
IPO_date = "2019年7月22日"  # 设置日期为"2019年7月22日"
exchange = "上海证券交易所"  # 设置交易所为"上海证券交易所"
capital = 10589819000  # 总股本:10589819000股
shares = 8621018000  # 流通股本:8621018000股
price_IPO = 5.85  # 设置当前价格为5.85
price_Jul22 = 12.27  # 设置当前价格为12.27
change_Jul22 = 1.09744  # 涨幅:109.744%
 
tup =(name,code,IPO_date,exchange,capital,shares,price_IPO,price_Jul22,change_Jul22)                  #创建一个元组,包括以上数据
print(tup)                                     #打印输出这个元组
    
print(tup[0])                                             #访问该元组的首个元素
print(tup[-1])                                             #访问该元组的末尾元素
print(tup[2:6])#访问该元组的第3个至第6个元素

预期输出(本机实际运行结果,具体以平台运行结果为准):

('中国通号', '688009', '2019年7月22日', '上海证券交易所', 10589819000, 8621018000, 5.85, 12.27, 1.09744)
中国通号
1.09744
('2019年7月22日', '上海证券交易所', 10589819000, 8621018000)

平台任务3(平台原始代码)

以下代码与教学平台任务要求完全一致:

任务要求:用 del 删除任务2中创建的元组 tup,再新增H股收盘价与H股涨跌幅两个字段,把十一个字段打包成新元组 tup_new 并输出。请将代码原样输入教学平台(注释除外),判定以平台为准。

列表 42.3: 平台原始代码 (3)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
name = "中国通号"  # 设置名称为"中国通号"
code = "688009"  # 设置代码为"688009"
IPO_date = "2019年7月22日"  # 设置日期为"2019年7月22日"
exchange = "上海证券交易所"  # 设置交易所为"上海证券交易所"
capital = 10589819000  # 总股本:10589819000股
shares = 8621018000  # 流通股本:8621018000股
price_IPO = 5.85  # 设置当前价格为5.85
price_Jul22 = 12.27  # 设置当前价格为12.27
change_Jul22 = 1.09744  # 涨幅:109.744%
tup = (name,code,IPO_date,exchange,capital,shares,price_IPO,price_Jul22,change_Jul22)  # 定义元组tup
 
price_Jul22_H = 5.43     #以浮点型输入中国通号H股7月22日收盘价
change_Jul22_H = -0.1171 #以浮点数输入中国通号H股7月22日涨跌幅
del tup                  #删除任务2中创建的元组
tup_new = (name,code,IPO_date,exchange,capital,shares,price_IPO,price_Jul22,change_Jul22,price_Jul22_H,change_Jul22_H)   #创建包含H股股价和涨跌幅的新元组
print(tup_new)  #打印这个新元组

预期输出(本机实际运行结果,具体以平台运行结果为准):

('中国通号', '688009', '2019年7月22日', '上海证券交易所', 10589819000, 8621018000, 5.85, 12.27, 1.09744, 5.43, -0.1171)
列表 42.4: 元组的多种创建方式
# 方式1: 使用圆括号创建元组
# 圆括号是可选的,但加上可以提高代码可读性
tup1 = ('physics', 'chemistry', 1997, 2000)  # 包含字符串和整数的混合元组
tup2 = (1, 2, 3, 4, 5)  # 纯整数元组
tup3 = "a", "b", "c", "d"  # 不加括号也可以,Python会自动识别为元组

# 创建空元组
# 空元组常用于初始化变量或作为函数默认参数
empty_tup = ()

# 创建单元素元组(注意逗号!)
# 这是一个常见的陷阱:单个元素后面必须有逗号,否则会被识别为带括号的表达式
single_tup = (50,)  # 正确:有逗号,是元组
not_a_tuple = (50)  # 错误:没有逗号,这只是整数50

# 打印结果以验证类型
print('元组1:', tup1)
print('元组2:', tup2)
print('元组3:', tup3)  # 证明不加括号也能创建元组
print('空元组:', empty_tup)
print('单元素元组:', single_tup)

方法2:使用tuple()构造函数

tuple()构造函数可以将其他可迭代对象转换为元组:

列表 42.5: 使用tuple()构造函数
# 从列表创建元组
list_data = [1, 2, 3, 4]
tup_from_list = tuple(list_data)

# 从字符串创建元组(每个字符成为一个元素)
tup_from_string = tuple('Hello')

# 从range对象创建元组
tup_from_range = tuple(range(5))

print('从列表:', tup_from_list)
print('从字符串:', tup_from_string)
print('从range:', tup_from_range)

方法3:元组推导式(Tuple Comprehension)

虽然Python没有专门的元组推导式语法,但可以使用生成器表达式配合tuple()函数:

列表 42.6: 创建元组的推导式写法
# 使用生成器表达式创建元组
# 生成器表达式用圆括号表示,然后传给tuple()函数
squares = tuple(x**2 for x in range(6))

# 这等价于先创建列表再转换,但更节省内存
# 因为生成器是惰性求值的,不需要创建中间列表

print('平方数元组:', squares)

创建方法的选择指南

场景 推荐方法 理由
已知元素,数量少 tup = (1, 2) 简洁直观
从其他序列转换 tuple(seq) 清晰表达意图
需要计算生成元素 tuple(x for x in ...) 内存高效
需要空元组 empty = () 最简洁

42.5 元组的索引与切片操作

作为有序数据结构,元组支持强大的索引和切片机制,这使得访问元组中的元素或子元组变得非常灵活和高效。

基本索引

元组的索引从0开始,支持正向索引(从左到右)和负向索引(从右到左):

列表 42.7: 元组索引与切片操作
# 创建示例元组
tup1 = ('physics', 'chemistry', 1997, 2000)  # 混合类型元组
tup2 = (1, 2, 3, 4, 5, 6, 7)  # 纯数字元组

# 正向索引:从0开始,从左向右数
print('tup1[0]:', tup1[0])  # 第一个元素:'physics'
print('tup1[1]:', tup1[1])  # 第二个元素:'chemistry'

# 负向索引:从-1开始,从右向左数
print('tup2[-1]:', tup2[-1])  # 最后一个元素:7
print('tup2[-2]:', tup2[-2])  # 倒数第二个元素:6

# 切片操作:获取子元组
# 语法:tup[start:end:step]
# start包含,end不包含(左闭右开区间)
print('tup2[1:5]:', tup2[1:5])  # 索引1到4的元素:(2, 3, 4, 5)
print('tup2[::2]:', tup2[::2])  # 每隔一个取一个:(1, 3, 5, 7)
print('tup2[::-1]:', tup2[::-1])  # 反转元组:(7, 6, 5, 4, 3, 2, 1)

切片操作的详细说明

切片是Python中最强大和最常用的特性之一,理解其工作原理对于高效数据处理至关重要:

  1. 基本切片 [start:end]:从start索引到end-1索引
  2. 带步长切片 [start:end:step]:按step步长取元素
  3. 省略参数:[:end]从头开始,[start:]到结尾,[::]全部
  4. 负步长:表示反向取元素,常用于反转序列

索引越界处理

与列表不同,元组的不可变性意味着其长度是固定的。访问不存在的索引会引发IndexError:

列表 42.8: 元组索引越界演示
tup = (1, 2, 3)

# 下面的代码会抛出IndexError
try:
    print(tup[10])  # 索引10不存在
except IndexError as e:
    print(f'错误:索引超出范围 - {e}')

为了避免索引越界,可以使用条件检查或异常处理,或者先检查元组长度。

42.6 元组的不可变性:原理与实践

元组的不可变性是其最核心的特征,理解这一特性的含义、原因和影响对于正确使用元组至关重要。

不可变性的含义

“不可变”并不意味着我们完全不能改变与元组相关的变量,而是指:

  1. 不能修改元组中的元素
  2. 不能删除元组中的元素
  3. 不能向元组中添加新元素
  4. 但可以重新给变量赋值一个新的元组
列表 42.9: 元组不可变性演示
# 创建一个元组
tup = (12, 34.56)

# 尝试修改元组的第一个元素
try:
    tup[0] = 20  # 这会抛出TypeError
except TypeError as e:
    print(f'错误类型: {type(e).__name__}')
    print(f'错误信息: {e}')
    print('结论:元组元素一旦创建就不能修改')

# 虽然不能修改,但可以连接两个元组创建新元组
# 注意:这不是修改原元组,而是创建了一个全新的元组
tup3 = tup + ('abc', 'xyz')
print('原元组:', tup)  # 原元组保持不变
print('新元组:', tup3)  # 这是一个新创建的元组

# 也可以通过重复创建新元组
tup4 = tup * 2  # 元组重复
print('重复元组:', tup4)

为什么需要不可变性?

不可变性在软件工程和数据科学中都有重要意义:

  1. 数据安全性:在金融应用中,某些数据(如历史交易记录)一旦生成就不应被修改。元组的不可变性提供了语言层面的保证。

  2. 多线程环境:不可变对象是线程安全的,多个线程可以同时访问而无需加锁,这在并行计算和高频交易系统中非常重要。

  3. 哈希要求:只有不可变对象才能被哈希,因此只有不可变对象才能作为字典的键或集合的元素。这对于构建查找表、缓存等数据结构至关重要。

  4. 函数式编程:不可变性是函数式编程的核心原则之一,它使得程序更易于推理、测试和并行化。

  5. 内存优化:由于元组不可变,Python可以对相同内容的元组进行 intern 优化,共享内存。

可变对象陷阱

需要注意的是,如果元组包含可变对象(如列表),那么这些可变对象的内容是可以修改的,例如对 t = (1, [2, 3]) 执行 t[1].append(4) 是合法的——不可变的是元组的结构,不是内层对象本身。这种情况下,建议要么确保元组只包含不可变对象,要么使用文档明确说明这种行为的限制。

42.7 元组在金融数据分析中的应用

元组在金融领域有广泛的应用场景,其不可变性和简洁性使其成为处理金融数据的理想选择。具体而言,不可变性在数据科学与金融分析中的特殊价值体现在:

  • 交易参数保护: 交易策略的核心参数(如止损线、杠杆比例)一旦设定就不应被意外修改
  • 数据完整性保证: 历史价格数据、公司基本信息等静态数据适合用元组存储
  • 多值返回: 金融函数经常需要返回多个相关值(如均值和标准差),元组是最自然的载体
  • 字典键的需求: 按日期-股票代码这样的组合查找数据时,元组键是必需的

应用1:交易参数配置

在量化交易系统中,策略参数一旦设定就不应被意外修改,元组提供了天然的保障:

列表 42.10: 元组在金融交易参数配置中的应用
# 定义交易策略的核心参数
# 使用元组确保这些参数不会被意外修改
trading_params = (
    100000,    # 初始资金(元)
    0.05,       # 预期年收益率
    30,         # 投资期限(年)
    0.6         # 股票配置比例
)

# 元组解包:将元组的元素分别赋值给变量
# 这种写法比分别访问元组元素更清晰
initial_capital, rate, years, stock_ratio = trading_params

# 格式化输出参数
# 使用f-string进行格式化,提高可读性
print(f'初始资金: {initial_capital:,.0f}元')  # 添加千位分隔符
print(f'年利率: {rate:.1%}')  # 百分比格式
print(f'投资期限: {years}年')
print(f'股票配置: {stock_ratio:.0%}')

# 计算期末资产
final_amount = initial_capital * (1 + rate) ** years
print(f'期末资产(复利): {final_amount:,.2f}元')

应用2:金融数据的键值对

当需要用复合键查找数据时,元组是不可替代的选择。例如,按照(股票代码, 日期)查找价格:

列表 42.11: 使用元组作为字典键
# 创建价格查找表
# 键是(股票代码, 日期)的元组,值是收盘价
price_lookup = {
    ('600519.SH', '2024-01-15'): 1856.00,
    ('600519.SH', '2024-01-16'): 1862.50,
    ('000858.SZ', '2024-01-15'): 158.20,
    ('000858.SZ', '2024-01-16'): 159.80
}

# 查询特定股票在特定日期的价格
stock = '600519.SH'
date = '2024-01-15'
price = price_lookup.get((stock, date))

if price:
    print(f'{stock}{date} 的收盘价是 {price:.2f}元')
else:
    print('未找到匹配的价格数据')

# 只有不可变对象(如元组)才能作为字典键
# 下面的代码会报错,因为列表是可变的:
# error_dict = {['600519.SH', '2024-01-15']: 1856.00}  # TypeError

应用3:函数返回多个值

金融函数经常需要返回多个相关的值,元组是最自然的方式:

列表 42.12: 函数返回多个值使用元组
def calculate_return_metrics(prices):
    '''
    计算价格序列的多个收益率指标

    参数:
        prices: 价格列表

    返回:
        (总收益率, 年化收益率, 波动率)的元组
    '''
    import numpy as np

    # 计算简单收益率
    returns = np.diff(prices) / prices[:-1]

    # 总收益率
    total_return = (prices[-1] - prices[0]) / prices[0]

    # 年化收益率(假设252个交易日)
    annual_return = (1 + total_return) ** (252 / len(prices)) - 1

    # 年化波动率
    annual_volatility = returns.std() * np.sqrt(252)

    return total_return, annual_return, annual_volatility

# 使用示例
stock_prices = [100, 102, 101, 105, 108, 107, 110]
total_ret, ann_ret, ann_vol = calculate_return_metrics(stock_prices)

print(f'总收益率: {total_ret:.2%}')
print(f'年化收益率: {ann_ret:.2%}')
print(f'年化波动率: {ann_vol:.2%}')

应用4:时间序列数据的不可变记录

在处理金融时间序列时,元组可以表示固定的记录:

列表 42.13: 使用元组表示时间序列数据点
# 定义一个时间戳数据点
tick = ('600519.SH', '2024-01-15 09:30:00', 1856.00, 100)
# 格式:(股票代码, 时间戳, 价格, 成交量)

# 元组解包提取字段
symbol, timestamp, price, volume = tick

print(f'股票代码: {symbol}')
print(f'时间戳: {timestamp}')
print(f'价格: {price:.2f}元')
print(f'成交量: {volume}手')

# 将多个tick数据组成列表
ticks = [
    ('600519.SH', '09:30:00', 1856.00, 100),
    ('600519.SH', '09:31:00', 1858.50, 150),
    ('600519.SH', '09:32:00', 1855.00, 80),
]

# 分析第一分钟的价格变化
price_change = ticks[1][2] - ticks[0][2]
print(f'第一分钟价格变化: {price_change:+.2f}元')

42.8 元组与列表的选择策略

理解何时使用元组、何时使用列表是编写Python代码的重要技能。以下是决策指南:

使用元组的场景:

  1. 数据需要保护:当数据是配置参数、常量,或需要防止意外修改时
  2. 需要字典键:当需要用复合数据作为字典的键时
  3. 性能敏感:当需要创建大量小对象,且不需要修改时
  4. 函数返回值:当函数需要返回多个值时
  5. 数据记录:当表示固定的记录结构(如数据库查询结果)时

使用列表的场景:

  1. 数据会变化:当需要频繁增删元素时
  2. 数据积累:当逐步构建数据序列时
  3. 内存缓冲:当需要临时存储中间结果时

性能对比

在内存使用和访问速度方面,元组通常优于列表:

列表 42.14: 元组与列表的性能对比
import sys
import timeit

# 创建相同内容的元组和列表
tup = tuple(range(1000))
lst = list(range(1000))

# 内存占用比较
print(f'元组内存占用: {sys.getsizeof(tup)} 字节')
print(f'列表内存占用: {sys.getsizeof(lst)} 字节')
print(f'元组更节省: {sys.getsizeof(lst) - sys.getsizeof(tup)} 字节')

# 访问速度比较
tup_time = timeit.timeit('x = tup[500]', globals=globals(), number=1000000)
lst_time = timeit.timeit('x = lst[500]', globals=globals(), number=1000000)

print(f'元组访问时间: {tup_time:.4f}秒')
print(f'列表访问时间: {lst_time:.4f}秒')
print(f'元组速度提升: {(lst_time/tup_time - 1)*100:.1f}%')

42.9 命名元组:增强版元组

Python的collections模块提供了namedtuple,它创建了一个带字段名的元组子类,结合了元组的不可变性和对象的属性访问便利性:

列表 42.15: 命名元组的使用
from collections import namedtuple

# 定义一个命名元组类型
# 相当于创建了一个简单的类
Stock = namedtuple('Stock', ['symbol', 'price', 'volume'])

# 创建Stock实例
stock1 = Stock('600519.SH', 1856.00, 1000)
stock2 = Stock(symbol='000858.SZ', price=158.20, volume=5000)

# 可以像元组一样索引访问
print(f'股票代码(索引): {stock1[0]}')

# 也可以像对象一样属性访问
print(f'股票代码(属性): {stock1.symbol}')
print(f'价格: {stock1.price}')
print(f'成交量: {stock1.volume}')

# 命名元组仍然是元组,是不可变的
print(f'是否为元组: {isinstance(stock1, tuple)}')

# 命名元组有友好的字符串表示
print(f'股票信息: {stock1}')

命名元组特别适合表示简单的数据记录,比普通元组更清晰,比定义完整的类更轻量。

42.10 延伸阅读(选读)

历史背景与理论发展

元组的概念起源于数学中的有序对(Ordered Pair)概念,由波兰-美国数学家阿尔弗雷德·塔尔斯基(Alfred Tarski)等人在20世纪初形式化。在编程语言领域,元组最早出现在Lisp语言中(1958年),随后被各种语言采纳。Python的元组设计深受函数式编程影响,其不可变性直接对应于数学中元组的定义——一旦创建,其元素和顺序就固定不变。

42.11 本章小结

要点:

  • 元组五大特性:不可变、有序、异构、轻量、可迭代;混合类型的”记录”用元组表达最自然
  • 创建方式:圆括号(可省略)、tuple(可迭代对象)tuple(生成器表达式);(50,) 才是单元素元组
  • 索引与切片和列表同构:tup[-1] 取末元素、tup[2:6] 取索引2~5、tup[::-1] 反转;越界抛 IndexError
  • del tup 删除的是整个变量绑定而非元素;元组不支持元素级增删改,但可用 +* 生成新元组
  • 元组解包可一次取出多个字段;命名元组 namedtuple 兼具索引访问与属性访问;(代码, 日期) 这类元组可作字典键

使用建议:

  • 不会改变的配置数据、函数的多值返回、需要作字典键的复合数据,优先用元组而非列表
  • 表示带字段名的固定记录时,用命名元组提高代码可读性
  • 用文档说明元组参数的结构和含义

易错点:

  • (50) 只是被括号包围的整数,(50,) 才是元组——单元素后的逗号不可省
  • tup[0] = 20del tup[0] 都会抛 TypeError;“修改”元组的正确方式是重建新元组再重新绑定变量
  • 拼接 tup + ('abc',) 右侧必须是元组,写成字符串会报错
  • 嵌套可变对象:t = (1, [2, 3]) 中执行 t[1].append(4) 是合法的——不可变的是元组的结构,不是内层对象本身

42.12 动手与思考

以下练习每题附参考答案(默认折叠)。请先独立完成并写下你的判断,再点开对照,最后上机验证。

  1. 概念自测(不看正课):元组的五个核心特性是什么?为什么元组能作字典的键而列表不能?del tupdel tup[0] 有什么区别?

    参考答案(点开前请先独立完成)

    解题思路:五个核心特性——不可变、有序、异构(可混合类型)、轻量(比列表省内存)、可迭代。元组能作字典的键是因为它不可变、可哈希,哈希值终生稳定;列表可变、不可哈希,内容一变哈希定位就失效,Python 直接禁止其作键。del tup 删除的是整个变量绑定(此后访问 tupNameError),合法;del tup[0] 试图删除元组内的元素,抛 TypeError——元组不支持元素级增删改。

    回扣主线章:元组与列表、字典的系统对比见第 章节 2 章。

  2. 输出预测:不运行代码,先写出下面代码的输出结果,再上机检验你的判断。

    t = ('600519', '贵州茅台', 1850.0, 100)
    print(t[-1])
    print(t[1:3])
    print(len(t))
    single = (50,)
    print(type(single).__name__)
    new_t = t[:2] + ('SH',)
    print(new_t)

    参考答案(先写下你的预测再点开)

    解题思路t[-1] 取末元素,输出 100t[1:3] 含头不含尾取索引 1、2,输出 ('贵州茅台', 1850.0)len(t) 输出 4(50,) 是带逗号的单元素元组,type(single).__name__ 输出 tuplet[:2] + ('SH',) 用拼接生成新元组(不修改原元组),输出 ('600519', '贵州茅台', 'SH')——注意 'SH', 的逗号不可省。

    # 验证脚本:原样运行题面代码
    t = ('600519', '贵州茅台', 1850.0, 100)  # 股票信息元组
    print(t[-1])  # 负索引取末元素
    print(t[1:3])  # 切片取索引1、2(含头不含尾)
    print(len(t))  # 输出元素个数
    single = (50,)  # 单元素元组,逗号不可省
    print(type(single).__name__)  # 输出类型名,避免打印带class前缀
    new_t = t[:2] + ('SH',)  # 拼接生成新元组
    print(new_t)  # 输出新元组

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    100
    ('贵州茅台', 1850.0)
    4
    tuple
    ('600519', '贵州茅台', 'SH')

    回扣主线章:元组索引、切片与单元素写法的讲解见第 章节 2 章。

  3. 动手验证:对 t = (12, 34.56),先尝试执行 t[0] = 20(捕获错误并说出错误类型),再执行 t = t + ('abc', 'xyz') 并输出 t,解释第二次”修改”为什么能成功。

    参考答案(点开前请先独立完成)

    解题思路t[0] = 20TypeError: 'tuple' object does not support item assignment——元组不可就地修改。t = t + ('abc', 'xyz') 能“成功”是因为它并非修改:+ 先创建全新元组 (12, 34.56, 'abc', 'xyz'),再把变量名 t 重新绑定到这个新对象;旧元组本身从未被改动,变的只是变量指向。

    # 验证脚本:先看元素级赋值报错,再看拼接重建
    t = (12, 34.56)  # 定义元组
    try:
        t[0] = 20  # 尝试就地修改元素(预期报错)
    except TypeError as error_msg:
        print('错误类型:', type(error_msg).__name__, '|', error_msg)  # 捕获并输出错误
    t = t + ('abc', 'xyz')  # 拼接生成新元组并重新绑定变量名t
    print(t)  # 输出新元组

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    错误类型: TypeError | 'tuple' object does not support item assignment
    (12, 34.56, 'abc', 'xyz')

    回扣主线章:不可变性与“重建再绑定”的讲解见第 章节 2 章。

  4. 变式任务(平台任务2、3同型改造):仿照平台任务2,为一只A股构建含(名称, 代码, 上市日期, 发行价, 现价, 涨跌幅)六个字段的元组,输出首元素、末元素与索引2~5的切片;再仿照平台任务3,del 掉该元组并重建一个追加”市盈率”字段的七字段新元组后输出。

    参考答案(点开前请先独立完成)

    解题思路:沿用平台任务的“中国通号”六字段数据(名称、代码、上市日期、发行价 5.85、现价 12.27、涨跌幅 1.09744)构建元组;首元素用 tup[0]、末元素用 tup[-1]、索引 2~5 的切片含头不含尾写 tup[2:6]。第二问先 del stock_tup 解除变量绑定,再重建含“市盈率”的七字段新元组(市盈率取示例值 33.06,重点在重建写法而非数值本身)。

    # 变式程序:六字段元组的访问,以及del后重建七字段元组
    stock_tup = ('中国通号', '688009', '2019年7月22日', 5.85, 12.27, 1.09744)  # 六字段股票信息元组
    print(stock_tup[0])  # 输出首元素(名称)
    print(stock_tup[-1])  # 输出末元素(涨跌幅)
    print(stock_tup[2:6])  # 输出索引2~5的切片(含头不含尾)
    del stock_tup  # 删除整个变量绑定
    stock_tup_new = ('中国通号', '688009', '2019年7月22日', 5.85, 12.27, 1.09744, 33.06)  # 重建并追加市盈率字段
    print(stock_tup_new)  # 输出七字段新元组

    预期输出(本机 peter 环境实际运行结果,具体以平台运行结果为准):

    中国通号
    1.09744
    ('2019年7月22日', 5.85, 12.27, 1.09744)
    ('中国通号', '688009', '2019年7月22日', 5.85, 12.27, 1.09744, 33.06)

    注意:以上为本题变式的独立代码;列表 42.2列表 42.3 对应平台任务的原始代码块仍须原样输入教学平台,不要用本变式替换。

    回扣主线章:同型字段打包与重建的完整解析见第 章节 2 章与本章正文。