实验专题(一):从列表到字典——手把手重构你的成绩录入系统
实验专题(一):从列表到字典——手把手重构你的成绩录入系统
当你的数据需要快速按“姓名”查找时,字典是比列表更优雅的数据结构。本文基于《Python 学习笔记(第五期)——组合数据类型》中关于列表和字典的核心知识,带你一步步将基于列表实现的成绩管理系统重构为基于字典的实现,并详细解释每一步的设计思路与坑点。
原实验:Python实战:手把手带你撸一个成绩管理系统(附源码+详细注释)
关联笔记:
- Python 学习笔记(第二期)——基础语法(下篇):运算符全解与实战演练 —— §5 成员运算符(
in/not in)- Python 学习笔记(第五期)——组合数据类型:列表、元组、集合与字典精讲 —— §2 列表、§5 字典、§8 可变与不可变类型
更多Python知识:Python基础入门
我的主页:AOwhisky,这里有更多运维系统性知识整理和其他有趣内容,欢迎与我一起探讨学习~
📑 实验目录
- 缘起:一个经典的小项目
- 目标:用字典重构数据结构
- 功能模块逐一改写
- 完整可运行代码
- 列表 vs 字典:对比总结
- 避坑指南:新手常犯的字典错误
- 扩展思考:如果允许重名怎么办?
- 知识点对应与总结
一、缘起:一个经典的小项目
很多 Python 初学者都会写一个“成绩录入系统”来练习流程控制和数据结构。最初我们可能用列表嵌套列表来存储学生信息,这也是第五期 §2.1中介绍的列表基本用法:
info=[['刘备',90,88,95],['张飞',70,75,80]]这种结构简单直接,但随着功能扩展,它暴露出几个明显的短板:
| 问题 | 说明 | 影响 |
|---|---|---|
| 查找效率低 | 按姓名查找成绩需要遍历整个列表,时间复杂度 O(n) | 数据量越大越慢 |
| 代码可读性差 | 通过下标i[0]、i[1]访问数据 | 一眼看不出i[2]是数学还是英语 |
| 维护成本高 | 字段顺序调整时,所有下标都要改动 | 极易引入 bug |
| 数据语义模糊 | 纯数字下标无法表达业务含义 | 代码难以理解和维护 |
而 Python 内置的字典(dict)——参见第五期 §5——天生就是为“键值对”查询设计的,用姓名作为键,成绩作为值,既快又清晰。于是,我决定将原系统用字典重写一遍,并在此记录下完整的改造过程。
二、目标:用字典重构数据结构
2.1 选择哪种字典结构?
在开始重构之前,我们先回顾第五期 §5.1中介绍的字典创建方式。根据业务需求,有两种常见设计:
| 方案 | 外层键 | 内层值 | 优点 | 缺点 |
|---|---|---|---|---|
| A | 学号(自动生成) | {'name':'刘备', 'chinese':90, ...} | 支持同名学生 | 查询时需要先遍历找姓名,稍麻烦 |
| B | 姓名 | {'chinese':90, 'maths':88, 'english':95} | 直接通过姓名访问,O(1)极速 | 不支持重名 |
因为题目明确要求“姓名重复则无法录入”,所以我选择方案B,直接用姓名作为外层键。这样后面所有功能(查询、修改、删除)都可以用if name in info一行搞定——这正是第二期 §5中介绍的成员运算符in在字典中的典型应用。
2.2 初始数据
基于方案 B,初始数据结构如下,注意这里使用了字典嵌套字典的结构(参见第五期 §5.3):
info={'刘备':{'chinese':90,'maths':88,'english':95},'张飞':{'chinese':70,'maths':75,'english':80}}数据结构可视化:
info (外层字典) ├── '刘备' (键) → {'chinese': 90, 'maths': 88, 'english': 95} (内层字典) │ ├── 'chinese' → 90 │ ├── 'maths' → 88 │ └── 'english' → 95 └── '张飞' (键) → {'chinese': 70, 'maths': 75, 'english': 80} ├── 'chinese' → 70 ├── 'maths' → 75 └── 'english' → 80三、功能模块逐一改写
原系统有五个功能:录入、查询(全部/个人)、修改、删除、退出。下面我们逐个用字典风格重写,并在每段代码中标注对应的笔记知识点。
3.1 成绩录入(对应 第五期 §5.3)
原列表做法:先检查姓名是否在列表中(遍历),若不在则构造列表[name, ch, ma, en]并追加。
字典做法:直接判断name in info(第二期 §5成员运算符),不存在则新增键值对(第五期 §5.3字典元素的添加)。
ifselect==1:name=input('请输入姓名:')ifnameininfo:# 成员运算符 in,时间复杂度 O(1)print('姓名已存在,请重新录入!')else:ch=int(input('请输入语文成绩:'))ma=int(input('请输入数学成绩:'))en=int(input('请输入英语成绩:'))# 字典添加新键值对(第五期 §5.3)info[name]={'chinese':ch,'maths':ma,'english':en}print(f'{name}成绩录入成功!')核心优势:in操作在字典中平均时间复杂度为O(1),远快于列表的O(n)。随着数据量增长,这个差距会越来越明显。
3.2 成绩查询(对应 第五期 §5.2)
3.2.1 查询全部成绩
遍历字典的items()方法(第五期 §5.2),同时拿到姓名和成绩字典:
ifsub=='1':print('姓名\t语文\t数学\t英语')forname,scoresininfo.items():# 遍历键值对print(f'{name}\t{scores["chinese"]}\t{scores["maths"]}\t{scores["english"]}')3.2.2 查询个人成绩
直接通过姓名键获取,然后打印各科成绩:
elifsub=='2':name=input('请输入姓名:')ifnameininfo:# 成员判断s=info[name]# 通过键访问值(第五期 §5.2)print(f'姓名:{name}')print(f'语文:{s["chinese"]}')print(f'数学:{s["maths"]}')print(f'英语:{s["english"]}')else:print('查无此人!')3.3 成绩修改(对应 第五期 §5.3)
先展示全部成绩,让用户看到现有数据;然后输入姓名,若存在则选择科目并赋新值。修改嵌套字典中的值时,直接通过info[name][key] = new_value即可:
ifselect==3:# 展示全部print('姓名\t语文\t数学\t英语')forname,scoresininfo.items():print(f'{name}\t{scores["chinese"]}\t{scores["maths"]}\t{scores["english"]}')name=input('请输入要修改的姓名:')ifnamenotininfo:print('查无此人!')else:print('1:语文;2:数学;3:英语')sub_idx=int(input('请输入修改学科编号:'))new_score=int(input('请输入新成绩:'))# 学科映射subject_map={1:'chinese',2:'maths',3:'english'}ifsub_idxinsubject_map:# 修改嵌套字典中的值(第五期 §5.3)info[name][subject_map[sub_idx]]=new_scoreprint('成绩修改成功!')else:print('学科编号错误!')⚠️ 注意:
info[name]获取的是内层字典(如{'chinese':90, 'maths':88, 'english':95}),然后通过[key]进一步定位到具体科目。这与第五期 §5.3中介绍的字典修改方式一致。
3.4 成绩删除(对应 第五期 §5.4)
同样先展示全部,然后按姓名删除条目。使用del语句删除字典的键值对(第五期 §5.4):
ifselect==4:print('姓名\t语文\t数学\t英语')forname,scoresininfo.items():print(f'{name}\t{scores["chinese"]}\t{scores["maths"]}\t{scores["english"]}')name=input('请输入要删除的姓名:')ifnameininfo:delinfo[name]# 删除该键值对(第五期 §5.4)print('删除成功!')else:print('查无此人!')⚠️ 安全提醒:删除不存在的键会抛出
KeyError,所以一定要先用in检查。这也在第五期 §5.4中有详细说明。
3.5 退出系统
ifselect==5:print('退出系统,欢迎再次使用!')break四、完整可运行代码
将以上所有片段整合,加上菜单循环,就是一套完整的字典版成绩录入系统。代码已测试通过,Python 3.6+ 均可运行。
# 成绩录入系统(字典版)menu=''' 1:成绩录入 2:成绩查询 3:成绩修改 4:成绩删除 5:退出系统 '''info={'刘备':{'chinese':90,'maths':88,'english':95},'张飞':{'chinese':70,'maths':75,'english':80}}whileTrue:print(menu)select=int(input('请输入管理选项:'))ifselect==1:name=input('请输入姓名:')ifnameininfo:print('姓名已存在,请重新录入!')else:ch=int(input('请输入语文成绩:'))ma=int(input('请输入数学成绩:'))en=int(input('请输入英语成绩:'))info[name]={'chinese':ch,'maths':ma,'english':en}print(f'{name}成绩录入成功!')elifselect==2:print('1:查询全部成绩;2:查询个人成绩')sub=input('请选择功能项:')ifsub=='1':print('姓名\t语文\t数学\t英语')forname,scoresininfo.items():print(f'{name}\t{scores["chinese"]}\t{scores["maths"]}\t{scores["english"]}')elifsub=='2':name=input('请输入姓名:')ifnameininfo:s=info[name]print(f'姓名:{name}')print(f'语文:{s["chinese"]}')print(f'数学:{s["maths"]}')print(f'英语:{s["english"]}')else:print('查无此人!')else:print('选项有误!')elifselect==3:print('姓名\t语文\t数学\t英语')forname,scoresininfo.items():print(f'{name}\t{scores["chinese"]}\t{scores["maths"]}\t{scores["english"]}')name=input('请输入要修改的姓名:')ifnamenotininfo:print('查无此人!')else:print('1:语文;2:数学;3:英语')sub_idx=int(input('请输入修改学科编号:'))new_score=int(input('请输入新成绩:'))subject_map={1:'chinese',2:'maths',3:'english'}ifsub_idxinsubject_map:info[name][subject_map[sub_idx]]=new_scoreprint('成绩修改成功!')else:print('学科编号错误!')elifselect==4:print('姓名\t语文\t数学\t英语')forname,scoresininfo.items():print(f'{name}\t{scores["chinese"]}\t{scores["maths"]}\t{scores["english"]}')name=input('请输入要删除的姓名:')ifnameininfo:delinfo[name]print('删除成功!')else:print('查无此人!')elifselect==5:print('退出系统,欢迎再次使用!')breakelse:print('无效选项,请重新输入!')五、列表 vs 字典:到底好在哪里?
结合第五期总结中的核心知识点汇总表,我们从多个维度对比两种实现方式:
| 对比维度 | 列表实现 | 字典实现 | 说明 |
|---|---|---|---|
| 查找速度 | O(n) 遍历 | O(1) 哈希查找 | 数据量越大差距越明显 |
| 代码可读性 | 依赖数字下标(i[1]、i[2]),易错 | 语义化键名,一目了然 | 可维护性大幅提升 |
| 扩展性 | 增加字段需修改所有下标 | 增加字段只需在内层字典加键 | 改动范围极小 |
| 数据完整性 | 无强制字段约束 | 可通过字典键名约束结构 | 减少数据不一致 bug |
| 内存占用 | 稍小(列表更紧凑) | 稍大(哈希表开销) | 对现代计算机可忽略 |
| 成员判断 | if name in [row[0] for row in info] | if name in info | 语法更简洁 |
对于这种“增删改查”为主的小系统,字典在速度上的优势可能并不明显(因为数据量小),但可维护性的提升是实打实的。当你后期想给每个学生增加“性别”字段时:
- 列表版:需要改动所有位置的下标,涉及大量代码修改
- 字典版:只需在内层字典加一个键即可,其余代码完全不受影响
六、避坑指南:新手常犯的字典错误
在改写过程中,有几个地方特别容易出错,结合第五期 §5和第五期 §8的知识点,我在此一一提醒:
| 序号 | 错误写法 | 正确写法 | 原因 |
|---|---|---|---|
| 1 | for i in info: print(i[0]) | for name, scores in info.items(): | 直接遍历字典拿到的是键(姓名),对字符串取下标会得到第一个字符 |
| 2 | if info.get(name) is not None: | if name in info: | 成绩可能为 0,get()会返回None造成误判 |
| 3 | info[name] = 100 | info[name]['chinese'] = 100 | 前者会覆盖整个内层字典,后者只修改指定科目 |
| 4 | del info[name]不检查 | if name in info: del info[name] | 删除不存在的键会抛出KeyError |
| 5 | score = input('成绩:') | score = int(input('成绩:')) | input()返回字符串,可能导致比较或计算错误 |
重点说明:
错误1涉及字典的遍历方式。在第五期 §5.2中我们强调过,直接遍历字典默认遍历的是键,而不是键值对。正确的做法是使用items()方法:
# ❌ 错误foriininfo:print(i)# 输出:刘备、张飞(姓名)print(i[0])# 输出:刘、张(姓名的第一个字符)# ✅ 正确forname,scoresininfo.items():print(name)# 输出:刘备、张飞print(scores)# 输出:{'chinese':90, 'maths':88, 'english':95}错误2涉及dict.get()方法的返回值陷阱。在第五期 §5.2中我们提到,get()方法在键不存在时返回None。但如果某个学生的某科成绩恰好为 0,get()返回0,与None不同,所以这个判断逻辑本身没问题——但更常见的问题是用get()判断键是否存在时,如果值可能是None或0,就会产生歧义。因此最安全的方式是直接使用in。
七、扩展思考:如果允许重名怎么办?
实际业务中,姓名重复很常见。若要支持重名,就不能用姓名当主键,而应该采用方案 A:用学号(或自动递增ID)作为外层键,内层字典包含姓名和成绩。
info={1001:{'name':'刘备','chinese':90,'maths':88,'english':95},1002:{'name':'刘备','chinese':70,'maths':75,'english':80},1003:{'name':'张飞','chinese':85,'maths':90,'english':78}}这种设计的查询流程变为:
# 按姓名查找(可能返回多条)results=[]forsid,dataininfo.items():ifdata['name']==name:results.append((sid,data))查询时根据姓名可能会搜出多条记录,需要进一步选择。这种设计更接近真实数据库,但代码复杂度会稍高一些,涉及第五期 §5.2中的字典遍历和条件筛选。
动手挑战:尝试基于上述数据结构,重写查询和删除功能,使其支持同名学生的处理。
八、知识点对应与总结
8.1 本文知识点与笔记章节对照
| 知识点 | 对应笔记 | 具体位置 |
|---|---|---|
| 列表的创建与访问 | 第五期 组合数据类型 | §2.1、§2.2 |
| 列表的遍历 | 第五期 组合数据类型 | §2.2 |
| 字典的创建与访问 | 第五期 组合数据类型 | §5.1、§5.2 |
| 字典的添加与修改 | 第五期 组合数据类型 | §5.3 |
| 字典的删除 | 第五期 组合数据类型 | §5.4 |
字典的遍历(items()) | 第五期 组合数据类型 | §5.2 |
成员判断in | 第二期 基础语法·下篇 | §5 成员运算符 |
| 可变与不可变类型 | 第五期 组合数据类型 | §8 补充知识 |
8.2 核心收获
通过这次重构,我们不仅将代码从“过程式下标操作”升级为“字典键值对操作”,更重要的是养成了一种“数据驱动”的编程思维:
选择合适的数据结构,往往能让代码更简洁、更健壮。
字典是 Python 中最常用的数据结构之一,熟练掌握它的遍历、增删改查、嵌套操作,是写好 Python 程序的必经之路。本文涉及的字典操作,在第五期 §5(字典)中都有系统讲解,建议结合笔记对照学习。
8.3 下一步学习建议
- 如果对字典的底层原理感兴趣,可以深入了解哈希表的工作机制——这是字典 O(1) 查找速度的根源
- 如果想进一步提升数据管理能力,可以学习Pandas库中的
DataFrame结构,它是二维数据处理的终极利器 - 如果想探索更复杂的嵌套结构,可以尝试实现“班级-学生-成绩”三级字典,并实现按班级统计平均分等功能
本文代码已测试通过,Python 3.6+ 均可运行。
如果觉得有用,点个赞再走吧 👍
