学生信息管理系统几乎是每个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或PowerShell,Mac和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, filename="students.json"): with open(filename, "w", encoding="utf-8") as f: json.dump(students, f, ensure_ascii=False, indent=2)ensure_ascii=False这个参数非常关键。不加的话,中文会被转成\uXXXX的转义形式,文件打开全是乱码一样的编码,虽然程序读回来没问题,但人工查看很不友好。加上之后中文正常显示。indent=2是让JSON格式化输出,每条记录占多行,可读性更好。
加载的逻辑要处理文件不存在的情况:
def load_data(filename="students.json"): try: with open(filename, "r", encoding="utf-8") as f: return json.load(f) except FileNotFoundError: return [] except json.JSONDecodeError: print("数据文件损坏,已初始化为空列表。") return []这里捕获了两个异常。FileNotFoundError是第一次运行程序时文件还不存在,返回空列表即可。json.JSONDecodeError是文件存在但内容不是合法JSON(比如手动编辑时改坏了),这时候也返回空列表,避免程序直接崩溃。
提示:保存操作建议在每次增删改之后都调用一次,而不是只在退出时保存。因为用户可能直接关窗口而不是走正常退出流程,那样数据就丢了。每次操作后保存虽然多几次文件写入,但对这个小系统来说性能完全不是问题。
4.2 输入校验:别让一个错误输入搞崩程序
新手写的程序最常见的崩溃原因就是输入没校验。比如年龄让输数字,用户输了个"abc",int("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里指定了encoding="utf-8",但如果你用某些编辑器打开生成的JSON文件发现中文乱码,可能是编辑器默认用了GBK编码打开。
这不是程序的问题,是编辑器的问题。在VS Code里,右下角可以切换文件编码,选"UTF-8"就能正常显示。如果要在代码里彻底避免这个问题,读写时都明确指定encoding="utf-8"就行,这一点前面保存和加载的代码里已经做了。
另外,如果你在Windows的cmd里运行程序,print中文可能出现乱码,这是因为cmd默认代码页不是UTF-8。可以在程序开头加一行:
import sys import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8")不过这个问题在Windows Terminal和PowerShell里一般不会出现,如果你用的是VS Code内置终端,通常也没问题。遇到乱码了再考虑加这段。
5.4 学号作为字符串的排序问题
前面说了学号用字符串存,好处是保留前导零。但字符串排序是按字典序来的,不是按数值大小。比如"10"排在"2"前面,因为字符"1"小于"2"。
如果你需要按学号排序显示,要么保证学号位数一致(比如都是7位),要么排序时转成整数:
students.sort(key=lambda 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, filename="students.csv"): if not students: print("暂无数据可导出。") return with open(filename, "w", newline="", encoding="utf-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.JSONDecodeError | JSON文件被手动改坏了 | 删除文件重新生成,或加异常捕获 |
ValueError: invalid literal for int() | 把非数字字符串转int | 加isdigit校验或try-except |
IndexError: list index out of range | 列表下标越界 | 检查pop或下标访问前是否判断了长度 |
| 中文乱码 | 编码不一致 | 读写文件统一用utf-8,CSV用utf-8-sig |
这张表里的前四个我都实际遇到过,尤其是KeyError,十有八九是字段名拼写不一致。建议把字段名定义成常量,比如FIELD_NAME = "name",用的时候引用常量,拼错的话编辑器会直接报错,比运行时才发现要好。
8. 关于这个项目我个人的一些体会
这个项目我从最早写一坨面条代码,到后来拆函数,再到用类重构,前后改了大概四五版。最大的体会是:代码结构的重要性远超功能多少。第一版我加了十几个功能,但全堆在一个文件里,后来想改个字段名,改了二十多处,还漏了两处导致bug。后来砍掉一半功能,把结构理清楚,反而更好维护,加新功能也快。
另一个体会是关于数据持久化。我早期版本没做持久化,每次测试都要重新录数据,录了删删了录,烦得不行。加上JSON保存之后,测试效率直接翻倍。所以哪怕你觉得"简易系统不用存文件",我也强烈建议加上,它带来的便利远超那十几行代码的成本。
还有就是输入校验,这个真的不能省。我见过太多新手程序,用户输错一个字符就整个崩掉,体验极差。加校验的代码不多,但能让程序从"玩具"变成"能用"。
如果你已经跟着写完了,建议再试着做两件事:一是把数据存储从JSON换成SQLite,感受一下数据库和文件存储的区别;二是给系统加个简单的登录功能,练一下密码校验和权限控制。这两个扩展都不难,但能让你对这个项目的理解再深一层。