news 2026/9/7 23:43:29

Python实战:从零开发一个命令行教务系统,串起面向对象与数据持久化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python实战:从零开发一个命令行教务系统,串起面向对象与数据持久化

前阵子帮几个朋友带了一轮 Python 入门,发现大家有个共同的坎:语法、列表、字典、函数都能背得出来,但一旦让写一个稍微完整的项目,就开始头大。正好那时候网上流传一道综合训练题——用 Py 写个简单的教务系统,我顺手就把完整版本写了出来。这个项目表面上是“管理系统”,实际上是一次很扎实的综合训练,基本上把 Python 里常用的面向对象、文件读写、数据校验、异常处理、循环分支这些知识点全部串了起来。做完以后最大的感受是:教务系统本身不复杂,但代码结构怎么设计、数据怎么存、流程怎么走,非常锻炼一个人的工程感觉。这篇文章把我做项目的完整过程拆开讲,适合已经学完 Python 基础的读者,也适合想找练手项目的同学照着做。

1. 先把需求想清楚:这个教务系统到底要管哪些事

1.1 从教务场景提炼核心实体

任何系统在写代码前,需求梳理永远排在第一位。教务系统的本质是“管理人和课之间的关系”,所以在动手前,我先把核心实体拆成了四块:

实体关键属性主要业务动作通常由谁执行
学生学号、姓名、性别、年级、专业注册、选课、退课、查成绩学生本人
教师工号、姓名、职称、所属院系查看授课名单、录入成绩教师
课程课程号、课程名、任课教师、学分、上课时间、容量创建、调整、停用管理员
选课记录学号、课程号、成绩、学期选课、退课、评分以上角色都会涉及

拆完这张表,系统里最重要的“关系”也就清楚了:学生和课程之间是多对多关系,中间需要一张选课记录来衔接;教师与课程之间的关系虽然看起来简单,但实际写到代码里时很容易被忽略——课程表里的“任课教师”字段一定要和教师列表保持一致性,否则会出现“课程上写着王老师,教师列表里却查无此人”的尴尬局面。

1.2 划清功能范围:第一版只做这些事

很多初学者有个误区,拿到一个“教务系统”的需求,就想把账号密码、权限分级、分页、搜索、导出报表全部做上。实际上作为综合训练项目,第一版要覆盖的核心功能不是堆砌功能点,而是把业务闭环走通。

必要功能是这些:

  • 学生信息:增、删、改、查
  • 课程信息:增、删、改、查
  • 选课:学生可选课、可退课,同一门课不能重复选
  • 成绩:教师按课程录入成绩,学生可查看自己成绩
  • 统计:课程平均分、最高分、最低分

不必要功能,或者说第二版再考虑的功能:

  • 真正的登录和密码验证(这里用三种角色的菜单模拟就行)
  • 并发控制(单机训练项目完全用不上)
  • 数据库(用 JSON 文件持久化就够,后面有需要再替换成 SQLite)

我当时就是按这个标准给自己划界。把无用装饰拆掉以后,整个项目的工作量控制在三天以内,而知识点覆盖一点没少,这才是综合训练的意义。

2. 存储方案决定代码边界:为什么选 JSON 而不是数据库

2.1 JSON 文件做持久化的三个理由

先回答一个最常见的问题:为什么不用 MySQL 或 SQLite?

因为这个项目是 Python 综合训练,重点不是数据库设计。上数据库会引入环境依赖、建表语句、事务思维、ORM 选型等一系列新概念,初学者很容易掉进“安装数据库”的坑,反而忽略了 Python 本身的能力。

JSON 文件在这个场景里是最合适的:

第一,Python 内建的 json 模块开箱即用,不需要任何安装步骤;

第二,JSON 的 dict/list 结构天然对应 Python 的数据类型,读写之后不需要做二次映射,新手理解起来零门槛;

第三,JSON 是纯文本,出问题可以直接打开看,删掉一行、改掉一个字段就能手动修复,调试体验比二进制格式好太多。

我用的数据文件结构是下面这个样子:

{ "students": [ { "student_id": "20240001", "name": "张三", "gender": "男", "grade": "2024", "major": "计算机科学与技术" } ], "teachers": [ { "teacher_id": "T001", "name": "李老师", "department": "计算机学院", "title": "副教授" } ], "courses": [ { "course_id": "CS101", "course_name": "Python编程", "teacher_id": "T001", "credit": 3, "time_slot": "周三第3-4节", "max_students": 40 } ], "enrollments": [ { "enrollment_id": 1, "student_id": "20240001", "course_id": "CS101", "semester": "2025春季", "score": null } ] }

字段设计上也有讲究。比如 enrollments 里我专门放了一个 enrollment_id。虽然它对训练项目来说有点“冗余”的嫌疑,但从扩展性看还是值得的——后面做退课、改成绩、按学期筛选时,都需要一条稳定且唯一的记录标识。

2.2 数据读写封装:写一个 DataStore 类集中处理

如果把读写 JSON 的逻辑分散在各个函数里,项目写着写着就会乱:有人用 json.dump,有人手动拼字典,有人直接改全局变量却忘了和文件同步,最后数据全散了。我的做法是把所有数据操作统一到一个 DataStore 类里,这样以后哪怕换存储方案,只需要改这个类,上层业务代码一行不用动。

import json import os class DataStore: def __init__(self, file_path="data.json"): self.file_path = file_path self.backup_path = file_path.replace(".json", "_backup.json") self.data = self.load_all() def load_all(self): if not os.path.exists(self.file_path): return {"students": [], "teachers": [], "courses": [], "enrollments": []} try: with open(self.file_path, "r", encoding="utf-8") as f: return json.load(f) except (json.JSONDecodeError, OSError): backup_data = self.load_backup() print("数据文件读取失败,已自动切换到备份数据") return backup_data def load_backup(self): if os.path.exists(self.backup_path): with open(self.backup_path, "r", encoding="utf-8") as f: return json.load(f) return {"students": [], "teachers": [], "courses": [], "enrollments": []} def save_all(self): # 先把原文件复制成备份,再写新文件 if os.path.exists(self.file_path): try: os.replace(self.file_path, self.backup_path) except OSError: pass with open(self.file_path, "w", encoding="utf-8") as f: json.dump(self.data, f, ensure_ascii=False, indent=2)

两个细节值得说一下。

第一,json.dump一定要加上ensure_ascii=Falseindent=2,否则中文全都会变成一串\uXXXX转义,打开文件根本没法看,调试的时候全靠猜。

第二,save_all的备份策略是用os.replace先把旧文件改成备份,再写新文件。这种做法避免了每保存一次就覆盖一次原文件,万一写到一半程序崩溃,还能从备份里恢复。很多初学者不会在意这一步,但真实系统里数据损坏这种事一旦遇到,有没有备份,心态完全是两回事。

3. 面向对象建模:按职责拆类,而不是把一切塞进函数

3.1 围绕角色和使用场景定义模型类

这个项目虽然是“综合训练”,但也没必要上特别复杂的架构。我用到的类数量很少,刚接触面向对象的人也能看懂。项目文件结构大概长这样:

teaching_management/ ├── main.py # 入口,负责菜单和交互 ├── models.py # Person / Student / Teacher / Course ├── service.py # BusinessService:核心业务逻辑 ├── data_store.py # DataStore:文件读写和备份 ├── data.json # 程序运行后自动生成 └── test_service.py # 单元测试

模型层的定义方式很朴素,就是基础的继承关系:

class Person: def __init__(self, person_id, name, gender): self.person_id = person_id self.name = name self.gender = gender class Student(Person): def __init__(self, student_id, name, gender, grade, major): super().__init__(student_id, name, gender) self.grade = grade self.major = major class Teacher(Person): def __init__(self, teacher_id, name, gender, department, title): super().__init__(teacher_id, name, gender) self.department = department self.title = title

有同学会问:Student 里有选课方法吗?我的答案是不放。这里其实触碰到了设计本质:Student 是“学生”这个实体的模型,它不应该知道完整的课程库、文件存储这些全局信息。如果让 Student 直接操作全局数据,测试和替换都非常麻烦。更干净的做法是让 BusinessService 接收 DataStore,在 Service 层完成校验和持久化。

3.2 Service 层和模型类分开放,后面测试省大事

把业务逻辑放在 Service 层,有一个非常实际的好处:隔离 IO。如果你的选课逻辑直接操作文件对象,将来写单元测试就得临时创建真实文件,麻烦且不稳定。而一旦逻辑都在 Service 里,测试的时候只需要构造一个内存里的 DataStore 再传进去,所有场景都可以干干净净地验证。

选课的核心方法大概长这样:

class BusinessService: def __init__(self, store: DataStore): self.store = store def enroll(self, student_id, course_id, semester="2025春季"): student = self.find_student(student_id) course = self.find_course(course_id) if not student: raise ValueError("学生不存在") if not course: raise ValueError("课程不存在") if self.is_already_enrolled(student_id, course_id): raise ValueError("已经选过这门课") if self.get_enroll_count(course_id) >= course["max_students"]: raise ValueError("课程容量已满") for e in self.get_student_enrollments(student_id): existing_course = self.find_course(e["course_id"]) if (existing_course and existing_course["time_slot"] == course["time_slot"]): raise ValueError( f"时间冲突:已选了《{existing_course['course_name']}》" ) # 通过所有校验后再加入数据 ...

这样做的好处一眼就能看出来:所有校验像一道一道闸门,任何一个条件不满足,函数直接抛出异常,后面的数据根本不会进入持久化。新手最常见的错误是先往列表里写记录,再在后面做校验,发现不对又删,最后 debug 半天都查不出来问题出在哪。

4. 三条关键业务链路:选课冲突检测、成绩录入和统计计算

4.1 选课冲突的三个边界,一开始就要堵住

选课是教务系统里最容易出错的地方,我整理了一张边界情况表:

场景处理方式为什么这样处理
重复选课先查 enrollments 中是否已有同课程记录防止数据重复,避免后续查询和报表时出现一行变两行
课程容量已满比较当前选课人数和 max_students超员问题在现实中非常常见,先挡住数据层的错误
上课时间冲突遍历学生已选课程,比对 time_slot 字段时间冲突最隐蔽,不校验就几乎一定会漏课

时间冲突这块值得展开讲。我用的是简单的前后字符串相等判断,比如“周三第3-4节”和“周三第3-4节”会冲突。如果课程表变成“周一1-2节”和“周一2-3节”,这种字符串比较自然就不够用了,得先把时段解析成(day, start_period, end_period)再去比较。但作为训练项目,第一版用字符串相等判断完全够用。我在代码上方留了注释,说明后续要支持更复杂的时间比较时,可以往哪个方向扩展。

4.2 成绩录入的“反复校验”和统计输出

成绩录入最容易踩的坑是“输入一个非法成绩就把前面的录入全部冲掉”。我的做法是给成绩录入单独写一个循环,只在当前这轮内反复输入,如果中途出错就重新输这条,不影响前面已经录好的数据。

课程成绩的录入代码差不多是这个逻辑:

def input_scores(self, course_id): students = self.get_course_students(course_id) if not students: print("这门课还没有学生选课") return for student in students: while True: try: raw = input( f"请输入 {student['name']}(学号 " f"{student['student_id']})的成绩:" ) score = float(raw.strip()) if not (0 <= score <= 100): print("成绩必须在 0 - 100 之间") continue self.set_score(student["student_id"], course_id, score) break except ValueError: print("输入格式不正确,请输入数字")

这里有一个细节:成绩用float而不是int。因为成绩可能出现 89.5 这种小数,如果用 int 会直接丢掉小数位,之后再改回来就麻烦了。另外成绩范围我限制在 0-100,目的不是刻板,而是避免教师录成绩时手滑敲错一个数字(比如误把 80 敲成 800)却没有察觉。

成绩统计的逻辑也不难,但有一个关键过滤条件:

def course_statistics(self, course_id): enrollments = [e for e in self.store.data["enrollments"] if e["course_id"] == course_id and e["score"] is not None] if not enrollments: return None scores = [e["score"] for e in enrollments] return { "平均分": round(sum(scores) / len(scores), 2), "最高分": max(scores), "最低分": min(scores), "参考人数": len(scores) }

统计之前一定要过滤掉score为 null 的选课记录。这个 null 其实就是学生选了课但老师还没给成绩,如果不过滤,平均分会被一堆无效数据拉低,明显是错误结果。

5. 命令行交互层的用户体验:菜单怎么设计,输入怎么防呆

5.1 用角色切换菜单替代复杂权限体系

很多人一拿到管理系统就想做“登录框”,但一个训练项目真没必要。我用的是最简单的“角色选择 + 菜单分支”方案:

启动程序以后,先问用户“你是谁”,提供三个选项:管理员、教师、学生。选完角色后进入对应主菜单。管理员可以管理学生和课程;教师可以查看自己课程的名单和录入成绩;学生可以选课、退课、查成绩。没有密码体系,所有操作都依赖使用者自觉选择身份。

有人觉得这样太简陋。但换成训练视角看,这恰恰是合理的:第一,省掉密码流程,把精力聚焦在业务逻辑上;第二,每个角色的操作边界很清楚,其实已经模拟了真实系统的权限划分思路,后续真要加强,只需把“选择角色”换成“账号 + 密码验证”。

入口代码大概长这样:

def main(): store = DataStore("data.json") service = BusinessService(store) while True: print("=== 教务系统主菜单 ===") print("1. 进入学生模式") print("2. 进入教师模式") print("3. 进入管理员模式") print("0. 退出系统") choice = input_choice("请选择:", {"0", "1", "2", "3"}) if choice == "0": break if choice == "1": student_menu(service) elif choice == "2": teacher_menu(service) else: admin_menu(service)

菜单用数字代替文字命令的好处是分支逻辑非常直接,不会出现“输入 addStudent 还是 add_student”这种歧义。第一次用的人不用看说明书也能操作。

5.2 输入健壮性:怎么让用户随便乱敲也不崩

命令行程序的体验上限,往往由输入处理决定。用户可能输入空字符串、字母、特殊符号、超长数字、负数,任何一个不小心都会让程序直接崩溃,带出一大段堆栈信息,观感很差。

我最常用的一个工具函数是input_choice

def input_choice(prompt, valid_options): while True: value = input(prompt).strip() if value in valid_options: return value print(f"输入无效,可选值:{'/'.join(valid_options)}")

对数字类输入,我会再封装input_positive_intinput_score,思路都一样:死循环里反复输入,遇到 ValueError 就提示重输,直到拿到合法值才 return。

输入处理这块,有两个经验值得单独说。

第一,input().strip()已经成了我的下意识动作。用户手滑在数字前后敲空格的情况太常见了,不strip()int(" 3 ")直接就会报错。

第二,尽量用while True循环配合break,不要用递归调用来重试。很多新手在输入不合法时会再次调用input,但递归层数一多就容易爆栈,而且局部变量会被重复创建,非常浪费。循环写法一眼能看懂,也是社区里最主流的做法。

6. 从“能跑”到“抗造”:数据备份、单元测试和扩展方向

6.1 防数据损坏的备份恢复机制

项目开发后期,我反复做了几次破坏性测试:手动把 data.json 改坏、删除 data.json、在数据文件里写入空内容,然后看程序是否还能启动。第一次确实崩了,于是我在load_all里加入了解析失败后自动加载备份的逻辑,然后打印一行警告提示,用户不会一脸懵地看到一串 traceback。

这套“启动即备份、写入先换名”的策略,在真实项目里也是常用做法。许多文件型存储的软件都有类似的机制。对训练项目来说,能在设计阶段就考虑到数据安全,已经是个明显的加分项。

6.2 最少但关键的单元测试:把核心逻辑锁死

这个项目我没写特别多的测试用例,但核心逻辑——选课、退课、成绩录入、统计——都配了一个简单测试。Python 自带 unittest,不需要装额外依赖。

测试的几个典型场景:

测试场景预期行为
学生选一门还没选过的课成功加入选课记录
学生重复选同一门课抛 ValueError,提示已选过
课程容量为 2,第三个人来选抛 ValueError,提示容量已满
两门课时间相同,学生再选第二门抛 ValueError,提示时间冲突
课程统计里某条记录成绩为空统计时不把这条记录算进去

测试里用的是假数据,构造方式就是新建一个 DataStore 指向临时文件,或者直接注入一个内存字典。因为 Service 和 DataStore 分离,测试代码才能写得这么干净。

我的一个真实经验是:写测试比写业务代码更考验“脑补场景”的能力。选课冲突的几种情况,如果不测一遍,很难保证后面需求迭代不引入 bug。我甚至遇到过这种事——某天给教师模块加功能,不小心把选课容量判断从<改成了<=,结果第 40 个学生还能选上第 41 个名额。没有测试,这类问题要等运行时才会暴露。

6.3 项目可以怎么延伸出去

如果让我说下一步最自然的扩展,是替换存储层。把 DataStore 里 load_all/save_all 的实现从 JSON 换成 SQLite 或者 MySQL,上层业务代码基本不用动,因为所有方法接口都一样。做一次这个动作,你会真正理解“解耦”这两个字的意思。

其次是加真正的身份认证。学生登录时输入学号,教师输入工号,管理员输入账号密码,然后把“选择角色”这一步拿掉。这个改动也能帮你搞清楚“认证”和“授权”到底差在哪。

再往后,可以用 Flask 或 FastAPI 把命令行版改造成 Web 版,前端配合简单的 HTML 模板,把录入、查询页面做出来。做完以后,你会明显感觉自己从“写脚本”的状态,正式进入了“写应用”的状态。

做这个项目期间,我最大的体会是:一个看起来“简单”的教务系统,真正到处都藏着细节。核心的业务闭环不复杂,但能不能把边界情况想清楚、数据结构定义得稳、分层设计得合理,直接决定了这个项目是“能跑”还是“能扛”。照着这套思路走,三天之内跑出一个完整版本应该没什么压力。如果你在实现过程里卡住了,优先回去检查两件事:数据到底有没有成功写进文件;选课冲突判断是不是提前return或者break了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 23:43:26

Java集合框架避坑指南:从List到HashMap源码与实战

自从环境变量配置折腾了一晚上终于搞定&#xff0c;把 “Hello World” 跑出来的那一刻&#xff0c;我真的觉得自己离 Java 大神不远了。前三弹里&#xff0c;我陆陆续续把运算符、流程控制、数组、方法、面向对象这些基础过了一遍&#xff0c;甚至冒泡排序也手动写了好几遍&am…

作者头像 李华
网站建设 2026/9/7 23:43:20

智能体赋能能源管理:从数据查询到主动诊断落地实践

简介&#xff1a;这份PDF聚焦研华iEMS.AI Agent能源智能体平台的设计与应用&#xff0c;面向能源管理、智能制造、工业自动化领域的技术人员、企业管理者及数字化转型负责人。内容围绕基于大语言模型的智能体技术&#xff0c;阐述如何以“AI大脑领域知识”构建能碳专家体系&…

作者头像 李华
网站建设 2026/9/7 23:36:13

Windows下Dify部署全指南:从Docker安装到Hackathon提速

简介&#xff1a;面向Windows开发者的Dify Hackathon安装部署教程文档&#xff0c;适合熟悉Git、Docker和Python、希望快速搭建Dify本地环境并参与Hackathon的技术人群。资源为1个docx文件&#xff0c;压缩包仅15KB&#xff0c;以文字步骤和命令说明为主。教程覆盖Windows 10/1…

作者头像 李华
网站建设 2026/9/7 23:35:15

基于Web的Java远程控制系统:架构设计与关键技术实现

简介&#xff1a;一份面向计算机相关专业毕业设计场景的完整论文与设计文档资源&#xff0c;围绕基于Web的远程控制系统展开&#xff0c;涵盖需求分析、Spring Boot后端、MySQL数据库、设备管理、日志记录及系统测试等核心环节&#xff0c;适合需要完成类似选题或学习远程控制项…

作者头像 李华
网站建设 2026/9/7 23:33:52

Pytest自动化测试实战:从fixture到Allure报告与CI集成

从我在一家电商公司第一次正经搭建自动化测试框架开始&#xff0c;Pytest就成了我工具箱里最顺手的那个工具。前后用纯Python做过UI自动化、接口自动化&#xff0c;也折腾过unittest、nose、Robot Framework&#xff0c;后来几乎所有新项目我都会毫不犹豫选Pytest。如果你正准备…

作者头像 李华
网站建设 2026/9/7 23:31:53

YOLOv8目标检测实战:从架构原理到数据集训练与部署

简介&#xff1a;面向需要利用YOLOv8训练自定义数据集的开发者&#xff0c;这份PDF围绕YOLOv8实例分割实战&#xff0c;先简要介绍YOLOv8的统一架构、训练效率、多任务支持等特点&#xff0c;再逐步演示在Ubuntu 22.04上完成环境配置并训练自建数据集的完整流程。内容涵盖NVIDI…

作者头像 李华