38  数据基本结构 元组、列表、字典

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

38.1 引言 Python数据结构在金融数据管理中的重要性

元组、列表和字典是Python管理金融数据的三种基本结构:元组适合存储不变配置(如股票代码、交易参数),列表适合管理时间序列数据(如价格历史、收益率序列),字典适合建立映射关系(如股票代码到名称、日期到行情)。本章用三个平台任务检验你对这三种结构的复习效果。

38.2 本章学习目标

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

  1. 能独立完成平台任务一:对存储10个交易日收盘数据的元组执行 tuple[5:9] 切片,并解释”左闭右开”与索引-日期的对应关系
  2. 能独立完成平台任务二:用正索引 list[3] 与负数切片 list[-3:] 访问列表元素,说出正、负索引各自的计数规则
  3. 能独立完成平台任务三:分别用 {} 字面量与 dict() 构造函数创建内容相同的字典,并用键 a['000'] 完成查询
  4. 能依据”是否按键快速查找、数据是否需要修改”在元组、列表、字典之间为金融数据(历史价格、实时持仓、代码-名称映射)选择合适的结构

若上述任务无法独立完成,请回到正课第2章对应小节补弱,再回到本章练习。

38.3 元组(Tuple)不可变序列

理论背景:不可变性的价值

元组是Python中不可变(Immutable)序列类型。不可变性看似是限制,实则是优势:

  1. 线程安全: 多线程环境下,不可变对象无需加锁
  2. 哈希能力: 可作为字典的键或集合的元素
  3. 内存效率: Python可以对不可变对象进行优化
  4. 契约保证: 保证数据不会被意外修改

补充说明:不可变性在金融系统中的意义

在交易系统中,订单一旦创建就不应被修改。如果需要更改,应该创建新订单。元组的不可变性正好契合这一需求:

# 不可变订单(推荐)
order = ('600000.SH', 1000, 10.25)  # (代码,数量,价格)

# 可变订单(危险)
order = ['600000.SH', 1000, 10.25]
order[1] = 2000  # 意外修改!可能造成重大损失

38.3.1 题目一 元组切片操作

在金融数据分析中,我们经常需要从历史价格数据中提取特定时间范围的数据进行切片分析。元组切片提供了一种高效的方式来访问序列的子集。

任务要求:给定存储10个交易日收盘数据的元组 tuple(索引09依次对应3月1日3月10日),要求用切片取出3月6日至3月9日(索引5~8)的数据并打印;关键在于按”左闭右开”写出正确的切片区间。请将代码原样输入教学平台(注释除外),判定以平台为准。

列表 38.1: 平台原始代码 (1)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确

# =============================================================================
# 题目一:元组切片操作
# =============================================================================
# 本任务演示如何使用元组存储股票价格数据,并通过切片操作提取特定日期范围的数据

# 场景说明:
# 某股票在连续10个交易日的收盘价数据(单位:元)
# 数据存储在元组中,表示这些历史数据不应该被修改

# 创建元组,存储10个交易日的股票收盘价
# 注意:这里的变量名tuple与Python内置函数tuple()重名,这不是最佳实践
# 但为了与教学平台保持一致,我们使用这个变量名
tuple=('4102','4382','4922','3975','3407','2894','3217','4926','4531','4557')
# 元组索引:  0     1     2     3     4     5     6     7     8     9
# 日期说明:  3/1   3/2   3/3   3/4   3/5   3/6   3/7   3/8   3/9   3/10

# 利用切片访问3月6日-3月9日的股票分价表数据
# 切片语法详解: [start:end:step]
#   - start: 起始索引(包含),如果省略则从0开始
#   - end: 结束索引(不包含),如果省略则到序列末尾
#   - step: 步长,如果省略则为1
#
# 本题要求:提取3月6日到3月9日的数据
#   - 3月6日对应索引5(值'2894')
#   - 3月9日对应索引8(值'4531')
#   - 因此切片范围为[5:9],包含索引5,不包含索引9
#
# 切片结果将包含索引5,6,7,8的元素,即'2894','3217','4926','4531'
print(tuple[5:9])
# 预期输出: ('2894', '3217', '4926', '4531')

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

('2894', '3217', '4926', '4531')

代码深度解析:

  1. 切片语法 [start:end:step]:

    • start: 起始索引(包含),省略则从0开始
    • end: 结束索引(不包含),省略则到末尾
    • step: 步长,省略则为1
    • 本例中: tuple[5:9] 获取索引5、6、7、8的元素
      • 索引5: ‘2894’ (3月6日)
      • 索引6: ‘3217’ (3月7日)
      • 索引7: ‘4926’ (3月8日)
      • 索引8: ‘4531’ (3月9日)
      • 不包含索引9
  2. 负数索引:

    tuple[-1]   # 最后一个元素('4557')
    tuple[-3:]  # 最后三个元素('4926', '4531', '4557')
    tuple[:-2]  # 除最后两个外的所有元素
  3. 元组的不可变性演示:

    try:
        tuple[0] = '5000'  # 尝试修改
    except TypeError as e:
        print(f'元组不可修改错误: {e}')
  4. 元组 vs 列表的选择:

    • 使用元组: 数据不应该被修改(如常量配置、历史数据)
    • 使用列表: 数据需要动态变化(如实时价格更新)

38.4 列表(List)可变序列

理论背景:动态数组的实现原理

Python列表是基于动态数组(Dynamic Array)实现的。其核心特性:

  1. 自动扩容: 当空间不足时,自动分配更大的内存块
  2. amortized O(1): 追加操作平均时间复杂度为O(1)
  3. 内存布局: 连续存储,缓存友好

数学分析:扩容策略

Python列表的扩容采用超额分配策略:

  • 新容量 ≈ 旧容量 × 1.125 + 常数
  • 这种策略平衡了内存使用和性能

38.4.1 题目二 列表索引操作

列表是Python中最常用的可变序列类型,在金融应用中广泛用于存储和管理时间序列数据。本任务演示如何通过索引访问列表中的特定元素。

任务要求:给定存储10个交易日收盘价的列表 list,要求先用正索引输出3月4日(索引3)的数据,再用负数切片输出最后三个交易日(3月8日~3月10日)的数据;需理解正索引从0向右数、负索引从-1向左数的计数规则。请将代码原样输入教学平台(注释除外),判定以平台为准。

列表 38.2: 平台原始代码 (2)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确

# =============================================================================
# 题目二:列表索引操作
# =============================================================================
# 本任务演示如何使用正数索引和负数索引访问列表元素

# 场景说明:
# 存储某股票10个交易日的收盘价数据
# 与元组不同,列表是可变的,可以随时添加、删除或修改元素

# 创建列表,存储股票价格数据
# 注意:变量名list与Python内置函数list()重名,这不是最佳实践
# 但为了与教学平台保持一致,我们使用这个变量名
list=['4102','4382','4922','3975','3407','2894','3217','4926','4531','4557']
# 列表索引:  0     1     2     3     4     5     6     7     8     9
# 日期说明:  3/1   3/2   3/3   3/4   3/5   3/6   3/7   3/8   3/9   3/10

# 利用索引访问3月4日的股票分时成交明细数据
# 注:按上方日期表,索引3对应3月4日;若要访问3月6日应使用list[5]
# 索引语法: list[index]
#   - index是元素在列表中的位置,从0开始计数
#   - list[3]获取索引为3的元素,即第4个元素(索引0,1,2,3)
#   - 这里返回'3975',表示3月4日的收盘价
print(list[3])
# 预期输出: 3975

# 利用负数索引访问最后三天的股票分时成交明细数据
# 负数索引语法: list[-n]
#   - -1表示最后一个元素
#   - -2表示倒数第二个元素
#   - -3表示倒数第三个元素
#   - 以此类推...
#
# 切片语法: list[-3:]
#   - -3作为起始位置,表示从倒数第3个元素开始
#   - 冒号后面省略,表示一直取到列表末尾
#   - 结果包含索引-3,-2,-1对应的元素
#   - 即'4926','4531','4557',分别对应3月8日、3月9日、3月10日的收盘价
print(list[-3:])
# 预期输出: ['4926', '4531', '4557']

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

3975
['4926', '4531', '4557']

代码深度解析:

  1. 正数索引:

    • list[3] 获取索引3的元素:‘3975’(3月4日数据;若要访问3月6日数据应使用list[5],即’2894’)
    • 索引从0开始,所以索引3是第4个元素
  2. 负数索引:

    • list[-1] 最后一个元素(‘4557’)
    • list[-3:] 最后三个元素
      • list[-3] = ‘4926’
      • list[-2] = ‘4531’
      • list[-1] = ‘4557’
  3. 列表的可变性:添加新价格:

    list.append('4800')  # 在末尾添加新元素
    print(list)  # 现在有11个元素
  4. 列表的时间复杂度: | 操作 | 时间复杂度 | 说明 | |——|———–|——| | append() | O(1)* | 平均情况,amortized | | pop() | O(1) | 弹出末尾元素 | | insert() | O(n) | 需要移动后续元素 | | del | O(n) | 同上 | | index() | O(n) | 线性搜索 | | in | O(n) | 同上 |

  5. 列表推导式的金融应用:

    # 计算日收益率
    prices = [10.20, 10.35, 10.15, 10.40, 10.30]
    returns = [(prices[i] - prices[i-1]) / prices[i-1]
               for i in range(1, len(prices))]
    print(f'日收益率: {returns}')

列表 vs NumPy数组:

特性 Python列表 NumPy数组
元素类型 可混合 必须相同
性能 较慢 极快(向量化)
内存 较大 紧凑
功能 通用 数值计算优化
金融应用 存储对象 价格/收益率计算

选择建议:

  • 数值计算(如收益率矩阵) → NumPy数组
  • 混合数据(如交易记录列表) → Python列表

38.5 字典(Dict)键值对映射

理论背景:哈希表的实现原理

Python字典基于哈希表(Hash Table)实现,核心思想:

  1. 哈希函数: hash(key) → 整数
  2. 数组存储: array[hash % size]
  3. 冲突解决: 开放寻址法或链表法

数学分析:时间复杂度

操作 平均情况 最坏情况 说明
插入 O(1) O(n) 哈希冲突时
查找 O(1) O(n) 同上
删除 O(1) O(n) 同上

实际应用中,Python的哈希函数设计良好,最坏情况极少出现。

补充说明:为什么字典查找如此快速?

假设我们有1000只股票:

  • 列表查找: 平均需要500次比较(O(n))
  • 字典查找: 只需1次哈希计算(O(1))

性能差异: 500倍!

38.5.1 题目三 字典的创建与查询

字典是Python中最重要的数据结构之一,它实现了键值对(Key-Value Pair)的映射关系。在金融应用中,字典广泛用于存储股票代码与公司名称的对应关系、配置参数、技术指标设置等信息。

任务要求:分别用 {} 字面量与 dict() 构造函数(逐个赋值)创建内容相同的市场代码字典,键为 '600'/'900'/'000'/'200'/'400',再用键 '000' 查询并输出对应的市场名称。请将代码原样输入教学平台(注释除外),判定以平台为准。

列表 38.3: 平台原始代码 (3)
# ⚠️ 平台原始代码 - 请原样输入至教学平台(注释除外),平台才会判定答案正确
#题目三
#{}方法创建字典
a = {'600':"上证A股",'900':"上证B股",'000':"深证A股",'200':"深证B股",'400':"三板市场股票"}

#dict()方法
b=dict()
b["600"] = "上证A股"  # 设置b["600"]为"上证A股"
b["900"] = "上证B股"  # 设置b["900"]为"上证B股"
b["000"] = "深证A股"  # 设置b["000"]为"深证A股"
b["200"] = "深证B股"  # 设置b["200"]为"深证B股"
b["400"] = "三板市场股票"  # 设置b["400"]为"三板市场股票"
#查询“000”开头对应股票
print(a['000'])

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

深证A股

代码深度解析:

  1. 字典的两种创建方法对比:

    • **方法1: 字面量 {}
      • 代码更简洁清晰
      • 一次性定义所有键值对
      • 推荐用于已知全部数据的情况
    • **方法2: dict()构造函数
      • 动态添加键值对
      • 适合逐步构建字典的场景
  2. 字典查询操作:

    # 直接访问(键不存在会报错)
    print(a['000'])  # '深证A股'
    
    # 安全访问(键不存在返回None或默认值)
    print(a.get('001', '未知'))  # '未知'
    
    # 检查键是否存在
    if '000' in a:
        print(a['000'])
  3. 字典的常用方法:

    # 获取所有键
    print(a.keys())  # dict_keys(['600', '900', '000', '200', '400'])
    
    # 获取所有值
    print(a.values())  # dict_values([...])
    
    # 获取所有键值对
    print(a.items())  # dict_items([('600', '上证A股'), ...])
  4. 字典的金融应用场景:

    • 股票代码→公司名称:

      code_to_name = {'600000': '浦发银行', '000001': '平安银行'}
    • 日期→行情数据:

      prices_by_date = {
          '2024-01-01': {'open': 10.20, 'close': 10.25},
          '2024-01-02': {'open': 10.25, 'close': 10.15}
      }
    • 技术指标→参数值:

      indicators = {
          'MA_short': 5,
          'MA_long': 20,
          'RSI_period': 14
      }

理论背景:数据结构的抽象数据类型(ADT)视角

在逐项对比三者之前,先从抽象数据类型(Abstract Data Type)的角度看这三种结构所代表的不同数据组织哲学:

数据结构 数学模型 核心操作 时间复杂度
元组 有序序列 索引访问、切片 O(1)
列表 可变序列 增删改查 O(1)~O(n)
字典 键值映射 哈希查找 O(1)平均

易混淆概念辨析:字典 vs 列表 vs 元组

特性 字典 列表 元组
符号 {} [] ()
可变性 可变 可变 不可变
索引 键(key) 位置(0,1,2,…) 位置
有序性 Python 3.7+有序 有序 有序
查找速度 O(1) O(n) O(n)
内存占用 较大 中等 最小
典型用途 键值映射 动态序列 固定配置

选择决策树:

是否需要通过键快速查找?
├─ 是 → 使用字典
└─ 否 → 数据是否需要修改?
    ├─ 是 → 使用列表
    └─ 否 → 使用元组

38.6 本章小结

要点:

  • 元组不可变、列表可变、字典按键存取;三者分别适合固定配置、动态序列与键值映射
  • 切片语法 [start:end] 含头不含尾:tuple[5:9] 取索引5、6、7、8共四个元素
  • 正索引从0向右数,负索引从-1向左数;list[-3:] 表示从倒数第三个取到末尾
  • 字典有两种创建方式:{} 字面量一次成型,dict() 加逐个赋值动态构建;查询用 d[键]
  • 查找效率:字典平均 O(1),列表与元组按位置/按值查找为 O(n)

易错点:

  • 变量名 tuplelist 会遮蔽Python内置类型;平台任务为与平台保持一致而使用,自己写代码时应避免
  • 切片终点不包含:提取3月6日至3月9日(索引5~8)四个数据用的是 [5:9],不是 [5:8][5:10]
  • print(list[3]) 输出 3975 不带引号,而 print(整个列表) 时字符串元素才带引号
  • 访问不存在的键 d[k] 会抛 KeyError,稳妥写法是 d.get(k, 默认值)

38.7 动手与思考

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

  1. 概念自测(不看正课):元组、列表、字典分别用什么符号创建?各自”可变/不可变”“按位置/按键”的性质如何?为什么字典的键必须是不可变类型?

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

    解题思路:结论——元组用圆括号 () 创建,列表用方括号 [],字典用花括号 {}(或 dict() 构造函数);元组不可变、按位置存取,列表可变、按位置存取,字典可变、按键存取。关键理由:字典按键存取依赖哈希表——先对键计算哈希值再定位存储位置,因此键必须是可哈希的不可变类型(字符串、数值、元组等);列表可变,内容一变哈希值就无法保持稳定,Python 因此把列表设为不可哈希,用作键会抛 TypeError

    回扣主线章:三种结构的系统讲解见第 章节 2 章。

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

    prices = ['4102', '4382', '4922', '3975', '3407', '2894', '3217', '4926', '4531', '4557']
    print(prices[5:9])
    print(prices[-3:])
    mkt = {'600': '上证A股', '000': '深证A股'}
    print(mkt['000'])
    print('900' in mkt)

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

    解题思路prices 是含 10 个字符串元素的列表,索引 0~9。第 1 个 print 执行 prices[5:9],切片含头不含尾,取索引 5、6、7、8 共四个元素,得到 ['2894', '3217', '4926', '4531'];第 2 个执行 prices[-3:],负索引从 -1 向左数,-3 对应索引 7,取到末尾,得到 ['4926', '4531', '4557'];第 3 个执行 mkt['000'],按键查询得到 深证A股;第 4 个执行 '900' in mktin 对字典检查的是键而非值,'900' 不是键,返回 False

    # 验证脚本:原样运行题面代码
    prices = ['4102', '4382', '4922', '3975', '3407', '2894', '3217', '4926', '4531', '4557']  # 10个交易日收盘数据
    print(prices[5:9])  # 切片取索引5、6、7、8(含头不含尾)
    print(prices[-3:])  # 负索引从倒数第三个取到末尾
    mkt = {'600': '上证A股', '000': '深证A股'}  # 市场代码映射字典
    print(mkt['000'])  # 按键查询深证A股
    print('900' in mkt)  # 判断'900'是否为字典的键

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

    ['2894', '3217', '4926', '4531']
    ['4926', '4531', '4557']
    深证A股
    False

    回扣主线章:切片“含头不含尾”与负索引的讲解见第 章节 2 章。

  3. 动手验证:对 t = (1, 2, 3) 执行 t[0] = 99 会发生什么?捕获该错误并说出错误类型;再把 t 换成列表重试同一操作,用一句话总结元组与列表的本质差异。

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

    解题思路t[0] = 99 抛出 TypeError: 'tuple' object does not support item assignment——元组创建后不支持元素级修改;换成列表 [1, 2, 3] 后同一操作成功,得到 [99, 2, 3]。一句话总结:本质差异在可变性——列表可就地修改元素,元组不能(“改”元组只能重建新元组再重新绑定变量)。

    # 验证脚本:对比元组与列表对同一赋值操作的反应
    t = (1, 2, 3)  # 定义元组
    try:
        t[0] = 99  # 尝试修改元组元素(预期报错)
    except TypeError as error_msg:
        print('错误类型:', type(error_msg).__name__, '|', error_msg)  # 捕获并输出错误类型与信息
    t_list = [1, 2, 3]  # 定义同内容列表
    t_list[0] = 99  # 列表支持就地修改
    print(t_list)  # 输出修改后的列表

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

    错误类型: TypeError | 'tuple' object does not support item assignment
    [99, 2, 3]

    回扣主线章:元组与列表的可变性对比见第 章节 2 章。

  4. 变式任务(平台任务一、三同型改造):用元组存储某指数连续8个交易日的收盘点数,输出第3至第6个交易日(对应索引2~5)的数据;再仿照平台任务三,用 dict() 构造函数构建板块代码映射(如 '300' 对应 '创业板''688' 对应 '科创板'),并查询 '300' 对应的板块。

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

    解题思路:第 3 至第 6 个交易日对应索引 2~5,按含头不含尾写切片 [2:6](不是 [2:5]);dict() 构造函数先创建空字典,再逐个赋值构建板块映射,查询仍用 d[键]

    # 变式程序:元组切片与dict()构造函数构建板块映射
    close_points = (3289, 3312, 3298, 3347, 3362, 3341, 3390, 3416)  # 连续8个交易日收盘点数
    print(close_points[2:6])  # 输出第3至第6个交易日(索引2~5)的数据
    board_map = dict()  # 用dict()构造函数创建空字典
    board_map['300'] = '创业板'  # 逐个赋值:300开头对应创业板
    board_map['688'] = '科创板'  # 逐个赋值:688开头对应科创板
    print(board_map['300'])  # 查询'300'对应的板块

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

    (3298, 3347, 3362, 3341)
    创业板

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

    回扣主线章:同型平台任务的完整解析见第 章节 2 章与本章正文。