ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Python学生信息管理系统实战:从零构建数据持久化与增删改查

Python学生信息管理系统实战:从零构建数据持久化与增删改查 学生信息管理系统几乎是每个Python初学者都会碰到的练手项目但大多数人写出来的版本都停留在能跑就行的阶段——数据存内存里一关程序全没了增删改查全靠一堆if-else堆着加个字段就要改十几个地方。我前后带过几批新人做这个项目也自己反复重构过好几版踩过的坑不算少。这篇文章就把我从零实现一个真正能用、结构清晰、数据不丢的学生信息管理系统的完整思路拆开讲一遍包括为什么这么设计、每一步怎么落地、哪些地方最容易翻车。不管你是刚学完Python基础语法想找个项目练手还是已经写过一版但觉得代码太乱想重构下面的内容都能直接参考。1. 先想清楚这个系统到底要解决什么问题1.1 学生信息管理系统的核心功能边界很多人一上来就写代码结果写到一半发现不知道该加什么功能、不该加什么功能最后要么功能太少显得简陋要么越加越多变成一个四不像。我的习惯是先把功能边界画清楚。一个简易学生信息管理系统核心就是围绕学生记录做四件事增、删、改、查。这四个字说起来简单但每一项背后都有需要想清楚的地方。增添加一条学生记录。需要确定学生有哪些字段——学号、姓名、性别、年龄、班级、专业这些是最基本的。学号通常作为唯一标识不允许重复。删根据学号删除一条记录。这里要考虑的是删除前要不要确认删错了能不能恢复。改根据学号修改某条记录的信息。是整体覆盖还是逐字段修改体验差别很大。查查看所有学生或者按学号、姓名等条件查找特定学生。除了这四个核心操作通常还会加一个显示全部的功能以及退出系统时的数据保存。这些加起来就是一个完整闭环了。我建议在动手之前拿张纸或者开个文本文件把这些功能一条条列出来标注哪些是必须的、哪些是锦上添花的。比如按班级统计人数就属于锦上添花第一版可以不做。先把核心闭环跑通比什么都重要。1.2 为什么选择用字典加列表来存数据数据怎么存是这个项目第一个要做的技术决策。常见的选择有这么几种存储方式优点缺点适用场景列表嵌套列表实现最简单字段靠下标访问可读性极差几乎不推荐列表嵌套字典字段名清晰操作灵活数据量大时查找慢简易系统首选字典嵌套字典按学号直接索引查找快遍历稍麻烦需要频繁按学号查SQLite数据库持久化好支持复杂查询需要学SQL对新手偏重数据量大或需要长期存储对于一个简易系统我强烈推荐列表嵌套字典的结构。每条学生记录是一个字典所有记录放在一个列表里。像这样students [ {id: 2024001, name: 张三, gender: 男, age: 18, major: 计算机科学}, {id: 2024002, name: 李四, gender: 女, age: 19, major: 软件工程}, ]为什么不用列表嵌套列表因为student[2]这种写法过两天你自己都不记得下标2代表什么字段了。用字典的话student[name]一目了然代码可读性直接上一个台阶。为什么不用字典嵌套字典虽然按学号查找是O(1)但遍历输出的时候要处理键值对而且学号作为键一旦录错了想改就很麻烦。列表嵌套字典在简易场景下是最平衡的选择。提示如果你后面想升级成数据库版本列表嵌套字典的结构可以很平滑地迁移过去每条字典对应数据库里的一行记录字段名对应列名几乎不用改业务逻辑。1.3 数据持久化为什么必须做怎么做最省事这是新手最容易忽略的一点。很多人写完增删改查运行测试没问题就以为大功告成了。结果关掉程序再打开数据全没了——因为数据只存在内存里。数据持久化说白了就是把内存里的数据写到硬盘上下次启动时再读回来。对于简易系统最省事的方案就是用JSON文件。为什么选JSON而不是CSV或纯文本三个原因Python内置json模块不需要装任何第三方库import json就能用。JSON天然支持嵌套结构列表套字典直接就能序列化不用自己设计格式。可读性好用文本编辑器打开就能看懂调试的时候很方便。具体做法就是在程序退出时把students列表写进一个.json文件程序启动时先尝试读取这个文件如果文件不存在就初始化为空列表。这个逻辑后面会给出完整代码。2. 从零搭建环境准备与项目骨架2.1 Python环境确认与版本选择在开始写代码之前先确认你的Python环境没问题。打开终端Windows下是cmd或PowerShellMac和Linux下是Terminal输入python --version如果显示Python 3.8以上的版本就没问题。我建议用3.8及以上因为这个项目里用到的一些语法特性比如f-string在3.6之后才完善而3.8是目前兼容性和稳定性都比较好的选择。如果你还没装Python去官网下载安装包安装时记得勾选Add Python to PATH这个选项。这一步非常关键不勾的话后面在终端里敲python命令会提示找不到。装完之后再敲一次python --version确认。编辑器方面用VS Code或者PyCharm都行。VS Code轻量装个Python插件就能跑PyCharm功能全对新手更友好但启动慢一些。选哪个不影响这个项目看你个人习惯。2.2 项目文件结构该怎么组织虽然是个小项目但文件结构还是值得花两分钟想一下。我见过太多人把所有代码堆在一个文件里写到后面自己都找不到函数在哪。我的建议是至少分成两个文件student_manager/ ├── main.py # 程序入口负责菜单和主循环 ├── student.py # 学生管理核心逻辑 └── students.json # 数据文件程序自动生成student.py里放所有跟学生数据操作相关的函数比如添加、删除、修改、查找、保存、加载。main.py里放菜单显示和用户交互的主循环。这样职责分明改功能的时候知道该去哪个文件改。如果你觉得两个文件还是麻烦一个文件也不是不行但至少要用函数把不同功能隔开不要全写在主循环里。这一点后面会详细说。2.3 定义学生记录的字段规范字段定得好不好直接影响后面写代码顺不顺。我建议第一版就定这几个字段id学号字符串类型。为什么用字符串不用整数因为学号可能有前导零比如001用整数存就变成1了显示出来不对。name姓名字符串。gender性别字符串男或女。age年龄整数。major专业字符串。字段确定之后建议写一个函数专门用来创建学生字典而不是每次添加的时候手动拼字典。这样做的好处是字段规范统一以后要加字段只改一个地方def create_student(id, name, gender, age, major): return { id: id, name: name, gender: gender, age: age, major: major }这个函数看起来简单但它是整个系统字段一致性的保障。我踩过的坑就是早期没写这个函数添加和修改两处各拼了一遍字典后来加了个入学年份字段只改了一处另一处忘了改结果修改功能保存后新字段就丢了排查了半天。3. 核心功能的实现逻辑与代码拆解3.1 添加学生去重校验比你想的重要添加功能看起来最简单但有个细节必须处理学号不能重复。如果不做校验同一个学号录进去两条记录后面按学号查找和删除都会出问题。实现思路是先遍历现有列表检查学号是否已存在存在就提示用户不存在才追加。def add_student(students): sid input(请输入学号).strip() for s in students: if s[id] sid: print(f学号 {sid} 已存在添加失败。) return name input(请输入姓名).strip() gender input(请输入性别).strip() age input(请输入年龄).strip() major input(请输入专业).strip() students.append(create_student(sid, name, gender, age, major)) print(添加成功。)这里有几个实操细节值得说。第一input()后面加.strip()去掉首尾空格用户不小心多敲了空格不会导致学号匹配失败。第二年龄这里我暂时用字符串接收没有强制转int因为如果用户输入了非数字int()会直接抛异常让程序崩溃。对于简易系统先存字符串也能跑但更好的做法是加个校验后面会讲。注意去重校验一定要在添加之前做不要先append再检查那样发现重复还得再删掉逻辑更绕。3.2 删除学生确认机制不能省删除操作最大的风险是误删。用户可能手滑输错了学号一按回车记录就没了。所以删除前必须让用户确认一次。def delete_student(students): sid input(请输入要删除的学号).strip() for i, s in enumerate(students): if s[id] sid: confirm input(f确认删除 {s[name]}学号{sid}(y/n)).strip().lower() if confirm y: students.pop(i) print(删除成功。) else: print(已取消删除。) return print(f未找到学号为 {sid} 的学生。)这里用enumerate同时拿到下标和元素找到后直接pop(i)删除。为什么不用remove(s)因为remove是按值删除如果列表里有两条完全相同的字典虽然学号去重后不会发生但逻辑上不严谨它会删错。用下标删除最精确。确认环节用lower()把输入统一转小写这样用户输入Y或y都能识别体验更好。3.3 修改学生逐字段修改比整体覆盖更友好修改功能有两种设计思路。一种是让用户把所有字段重新输一遍整体覆盖另一种是显示当前值用户想改哪个改哪个直接回车就保留原值。第二种体验好得多也是我推荐的做法。def update_student(students): sid input(请输入要修改的学号).strip() for s in students: if s[id] sid: print(f当前信息{s}) name input(f姓名{s[name]}).strip() if name: s[name] name gender input(f性别{s[gender]}).strip() if gender: s[gender] gender age input(f年龄{s[age]}).strip() if age: s[age] age major input(f专业{s[major]}).strip() if major: s[major] major print(修改成功。) return print(f未找到学号为 {sid} 的学生。)核心逻辑就是input的提示里带上当前值用户直接回车输入为空字符串就跳过不修改输入了内容才更新。这样改一个字段不用把其他字段都重敲一遍。学号本身我建议不允许修改因为它是对外索引的标识。如果确实要改学号那相当于删一条加一条逻辑上更清晰。3.4 查询与展示格式化输出让数据一目了然查询功能分两种查看全部和按条件查找。查看全部比较简单遍历列表逐条打印就行。但直接print(students)出来的格式很难看需要格式化。def show_all(students): if not students: print(暂无学生记录。) return print(f{学号:12}{姓名:8}{性别:6}{年龄:6}{专业:16}) print(- * 50) for s in students: print(f{s[id]:12}{s[name]:8}{s[gender]:6}{s[age]:6}{s[major]:16})这里用了f-string的对齐语法:12意思是左对齐占12个字符宽度。中文字符的宽度问题这里先不深究简易系统够用了。表头和数据行用同样的宽度看起来就是一张整齐的表格。按条件查找的话可以支持按学号精确查找和按姓名模糊查找def search_student(students): keyword input(请输入学号或姓名关键词).strip() results [s for s in students if keyword in s[id] or keyword in s[name]] if results: for s in results: print(s) else: print(未找到匹配的学生。)用列表推导式一行搞定筛选keyword in s[id]这种写法既能精确匹配学号也能模糊匹配姓名一个输入框搞定两种查找对用户来说更省事。4. 数据持久化与异常处理让系统真正可用4.1 JSON读写保存与加载的完整实现前面说了用JSON做持久化这里给出完整实现。保存就是把列表写进文件import json def save_data(students, filenamestudents.json): with open(filename, w, encodingutf-8) as f: json.dump(students, f, ensure_asciiFalse, indent2)ensure_asciiFalse这个参数非常关键。不加的话中文会被转成\uXXXX的转义形式文件打开全是乱码一样的编码虽然程序读回来没问题但人工查看很不友好。加上之后中文正常显示。indent2是让JSON格式化输出每条记录占多行可读性更好。加载的逻辑要处理文件不存在的情况def load_data(filenamestudents.json): try: with open(filename, r, encodingutf-8) as f: return json.load(f) except FileNotFoundError: return [] except json.JSONDecodeError: print(数据文件损坏已初始化为空列表。) return []这里捕获了两个异常。FileNotFoundError是第一次运行程序时文件还不存在返回空列表即可。json.JSONDecodeError是文件存在但内容不是合法JSON比如手动编辑时改坏了这时候也返回空列表避免程序直接崩溃。提示保存操作建议在每次增删改之后都调用一次而不是只在退出时保存。因为用户可能直接关窗口而不是走正常退出流程那样数据就丢了。每次操作后保存虽然多几次文件写入但对这个小系统来说性能完全不是问题。4.2 输入校验别让一个错误输入搞崩程序新手写的程序最常见的崩溃原因就是输入没校验。比如年龄让输数字用户输了个abcint(abc)直接抛ValueError程序就挂了。解决办法有两种。一种是用try-except包裹def input_age(): while True: age_str input(请输入年龄).strip() try: age int(age_str) if 0 age 150: return age print(年龄应在1到149之间请重新输入。) except ValueError: print(年龄必须是数字请重新输入。)用while True循环只有输入合法才return退出循环否则一直提示重输。这样用户输错多少次都不会让程序崩溃。另一种更简单的做法是用字符串的.isdigit()方法判断age_str input(请输入年龄).strip() if not age_str.isdigit(): print(年龄必须是数字。) return age int(age_str).isdigit()只能判断正整数不支持负数和小数但对年龄这个场景够用了。两种方式都行看你觉得哪种写起来顺手。4.3 主循环设计菜单驱动的交互框架把所有功能函数写好之后需要一个主循环把它们串起来。核心就是一个while True循环每次显示菜单根据用户输入调用对应函数。def main(): students load_data() while True: print(\n 学生信息管理系统 ) print(1. 添加学生) print(2. 删除学生) print(3. 修改学生) print(4. 查看全部) print(5. 查找学生) print(0. 退出系统) choice input(请选择操作).strip() if choice 1: add_student(students) save_data(students) elif choice 2: delete_student(students) save_data(students) elif choice 3: update_student(students) save_data(students) elif choice 4: show_all(students) elif choice 5: search_student(students) elif choice 0: save_data(students) print(数据已保存再见。) break else: print(无效选项请重新选择。) if __name__ __main__: main()这个结构清晰的地方在于主循环只负责分发具体逻辑都在各自的函数里。以后要加功能比如加个按班级统计只需要写个新函数然后在菜单里加一个分支就行不用动其他代码。if __name__ __main__:这行是Python的惯用写法意思是只有直接运行这个文件时才执行main()如果被其他文件import则不执行。养成这个习惯对以后写更大的项目有好处。5. 那些只有踩过才知道的坑5.1 字典在列表中的引用陷阱这个坑我在带新人时见过不止一次。看下面这段代码student {id: , name: , gender: , age: , major: } students [] for i in range(3): student[id] input(学号) student[name] input(姓名) students.append(student)看起来没问题对吧但实际上students列表里三个元素指向的是同一个字典对象。因为字典是可变对象append进去的是引用而不是副本。循环结束后修改student列表里所有元素都会跟着变最后三条记录全一样。正确做法是每次循环都创建一个新字典for i in range(3): student {} student[id] input(学号) student[name] input(姓名) students.append(student)或者用前面写的create_student函数每次调用都返回一个新字典天然避免这个问题。这也是我推荐用工厂函数创建字典的原因之一。5.2 文件路径问题相对路径的坑open(students.json, w)用的是相对路径相对于当前工作目录而不是脚本文件所在的目录。这意味着如果你在A目录下运行脚本数据文件就生成在A目录换个目录运行又会在新目录生成一个空的数据文件之前的数据就丢了。解决办法是用绝对路径基于脚本文件的位置来定位数据文件import os BASE_DIR os.path.dirname(os.path.abspath(__file__)) DATA_FILE os.path.join(BASE_DIR, students.json)__file__是当前脚本的路径os.path.abspath转成绝对路径os.path.dirname取所在目录最后拼上文件名。这样不管在哪个目录下运行脚本数据文件始终在脚本旁边不会出现数据丢失的假象。5.3 中文编码问题Windows下的特殊处理在Windows系统上有时候用open读写文件会遇到编码问题。虽然我们在open里指定了encodingutf-8但如果你用某些编辑器打开生成的JSON文件发现中文乱码可能是编辑器默认用了GBK编码打开。这不是程序的问题是编辑器的问题。在VS Code里右下角可以切换文件编码选UTF-8就能正常显示。如果要在代码里彻底避免这个问题读写时都明确指定encodingutf-8就行这一点前面保存和加载的代码里已经做了。另外如果你在Windows的cmd里运行程序print中文可能出现乱码这是因为cmd默认代码页不是UTF-8。可以在程序开头加一行import sys import io sys.stdout io.TextIOWrapper(sys.stdout.buffer, encodingutf-8)不过这个问题在Windows Terminal和PowerShell里一般不会出现如果你用的是VS Code内置终端通常也没问题。遇到乱码了再考虑加这段。5.4 学号作为字符串的排序问题前面说了学号用字符串存好处是保留前导零。但字符串排序是按字典序来的不是按数值大小。比如10排在2前面因为字符1小于2。如果你需要按学号排序显示要么保证学号位数一致比如都是7位要么排序时转成整数students.sort(keylambda s: int(s[id]))用key参数指定排序依据lambda里把学号转成整数再比较。这样2就会排在10前面了。当然前提是学号都是纯数字如果学号里包含字母这个转换会报错那就只能按字符串排了。6. 从能用走向好用几个值得加的改进6.1 用类来重构面向对象的写法前面所有功能都是函数加列表的实现方式对于简易系统完全够用。但如果你想借此机会练一下面向对象可以把学生和管理器都封装成类。class Student: def __init__(self, id, name, gender, age, major): self.id id self.name name self.gender gender self.age age self.major major def to_dict(self): return {id: self.id, name: self.name, gender: self.gender, age: self.age, major: self.major} classmethod def from_dict(cls, d): return cls(d[id], d[name], d[gender], d[age], d[major])to_dict和from_dict这一对方法负责在对象和字典之间转换这样JSON持久化的逻辑不用改还是存字典只是内存里用对象操作。管理器类则把增删改查都收进去class StudentManager: def __init__(self): self.students [] def add(self, student): if any(s.id student.id for s in self.students): raise ValueError(f学号 {student.id} 已存在) self.students.append(student)用类的好处是状态和方法绑定在一起不用每次调用函数都传students列表。而且以后要扩展成多个管理器比如不同班级各一个天然支持。不过我要说句实话对于这个简易系统面向对象并不是必须的。函数式写法更直观新手更容易理解。等你觉得函数式写法开始变得别扭了再重构成类也不迟。为了用类而用类反而增加理解成本。6.2 加一个简单的统计功能统计功能不难但很实用。比如统计总人数、男女比例、各专业人数def show_statistics(students): total len(students) if total 0: print(暂无数据。) return male sum(1 for s in students if s[gender] 男) female total - male print(f总人数{total}) print(f男生{male}女生{female}) majors {} for s in students: majors[s[major]] majors.get(s[major], 0) 1 print(各专业人数) for major, count in majors.items(): print(f {major}{count}人)majors.get(s[major], 0) 1这个写法是统计频次的经典套路字典里有这个键就取出来加1没有就默认0加1。比先判断键是否存在再操作要简洁。6.3 数据导出为CSV方便用Excel打开JSON适合程序读写但如果要把数据给别人看CSV更通用Excel直接就能打开。Python内置csv模块导出很简单import csv def export_csv(students, filenamestudents.csv): if not students: print(暂无数据可导出。) return with open(filename, w, newline, encodingutf-8-sig) as f: writer csv.DictWriter(f, fieldnames[id, name, gender, age, major]) writer.writeheader() writer.writerows(students) print(f已导出到 {filename})注意这里编码用的是utf-8-sig而不是utf-8。utf-8-sig会带一个BOM头Excel打开时才能正确识别中文否则可能乱码。这是Windows下用Excel打开CSV的一个经典坑很多人导出后中文全是乱码就是编码没选对。newline这个参数也是必须的不加的话在Windows上每行之间会多一个空行这是csv模块和Windows换行符配合时的已知问题。6.4 用prettytable美化输出如果觉得手动对齐太麻烦可以装个第三方库prettytable输出自动对齐还支持各种边框样式pip install prettytablefrom prettytable import PrettyTable def show_all_pretty(students): table PrettyTable() table.field_names [学号, 姓名, 性别, 年龄, 专业] for s in students: table.add_row([s[id], s[name], s[gender], s[age], s[major]]) print(table)输出效果比手动拼字符串好很多而且不用操心对齐宽度。不过这就引入了第三方依赖如果你的环境不方便装库手动格式化也完全够用。7. 调试与测试怎么确认系统真的没问题7.1 手动测试的完整流程写完代码别急着说做好了按下面的流程走一遍基本能覆盖所有主要路径首次运行确认数据文件自动创建查看全部显示暂无记录。添加三条记录学号分别为001、002、003确认添加成功。再次添加学号001确认提示已存在。查看全部确认三条记录格式整齐。修改002的姓名其他字段直接回车确认只改了姓名。删除003输入n取消确认记录还在再删一次输入y确认删除成功。查找00确认能匹配到001和002。退出程序重新运行查看全部确认数据还在。手动打开JSON文件确认中文正常显示。这九步走完核心功能基本都验证到了。我见过很多人只测了添加和查看就交付了结果删除和修改里的bug一直没发现。7.2 用断言做简单的自动化测试手动测试每次改代码都要重来一遍很烦。可以写几个简单的测试函数用assert验证逻辑def test_add_and_find(): students [] students.append(create_student(001, 测试, 男, 18, 测试专业)) assert len(students) 1 assert students[0][name] 测试 print(test_add_and_find passed) def test_duplicate_check(): students [create_student(001, 张三, 男, 18, 计算机)] sid 001 exists any(s[id] sid for s in students) assert exists True print(test_duplicate_check passed) if __name__ __main__: test_add_and_find() test_duplicate_check()这些测试不依赖用户输入直接验证核心逻辑。每次改完代码跑一遍几秒钟就能确认没改坏东西。虽然简陋但比手动点菜单快多了。7.3 常见报错与排查思路报错信息常见原因解决办法KeyError: name字典里没有这个键可能字段名拼错检查字段名拼写确认create_student里的键一致FileNotFoundError数据文件路径不对用绝对路径基于__file__定位json.JSONDecodeErrorJSON文件被手动改坏了删除文件重新生成或加异常捕获ValueError: invalid literal for int()把非数字字符串转int加isdigit校验或try-exceptIndexError: list index out of range列表下标越界检查pop或下标访问前是否判断了长度中文乱码编码不一致读写文件统一用utf-8CSV用utf-8-sig这张表里的前四个我都实际遇到过尤其是KeyError十有八九是字段名拼写不一致。建议把字段名定义成常量比如FIELD_NAME name用的时候引用常量拼错的话编辑器会直接报错比运行时才发现要好。8. 关于这个项目我个人的一些体会这个项目我从最早写一坨面条代码到后来拆函数再到用类重构前后改了大概四五版。最大的体会是代码结构的重要性远超功能多少。第一版我加了十几个功能但全堆在一个文件里后来想改个字段名改了二十多处还漏了两处导致bug。后来砍掉一半功能把结构理清楚反而更好维护加新功能也快。另一个体会是关于数据持久化。我早期版本没做持久化每次测试都要重新录数据录了删删了录烦得不行。加上JSON保存之后测试效率直接翻倍。所以哪怕你觉得简易系统不用存文件我也强烈建议加上它带来的便利远超那十几行代码的成本。还有就是输入校验这个真的不能省。我见过太多新手程序用户输错一个字符就整个崩掉体验极差。加校验的代码不多但能让程序从玩具变成能用。如果你已经跟着写完了建议再试着做两件事一是把数据存储从JSON换成SQLite感受一下数据库和文件存储的区别二是给系统加个简单的登录功能练一下密码校验和权限控制。这两个扩展都不难但能让你对这个项目的理解再深一层。
返回列表