☰
Python大作业实战:课程选课系统的JSON持久化与业务逻辑设计
2026/10/27 8:34:42 网站建设 项目流程

简介:这是一个面向Python初学者的课程设计大作业完整源码,实现了一套覆盖学校、学员、课程、讲师、管理员五种角色的选课系统。管理员可创建武汉、长沙两所学校,为Linux、Python、C语言课程设置校区、价格与周期;学员注册时选择学校并关联班级,讲师则能添加授课课程、查看班级学员列表并修改成绩,完整覆盖了学员、讲师、管理三类视图需求。项目全程使用pickle序列化将操作数据写入文件,清楚示范了Python对象持久化以及多角色交互逻辑,数据文件、运行截图和说明文档相互配合,便于对照界面理解每一步操作。压缩包共53个文件,包含13个Python源码、12个pyc编译缓存、18张界面运行截图,并附有大作业要求docx与README说明,整体约759KB;源码按core、interface、db、conf分层组织,便于阅读和扩展。已有53人学习下载,适合需要完成同类课程设计,或希望以实际项目学习Python面向对象与分层开发的学习者。

1. 课程选课系统:Python大作业里最卷的题目,也是最能拉开分差的工程试金石

每学期期末,课程选课系统都是Python程序设计大作业里被选得最多的题目之一。这个题目看似简单——不就是“管理员录入课程、学生选课退课”吗?实际上,一次完整的选课系统练习能覆盖数据建模、文件持久化、业务规则校验和交互设计一整条链路,几乎把一门Python课的重点全串起来了。你可以在内存里用列表糊一个能点的版本,但那样重启就丢数据;也可以做成带冲突检测、容量控制、JSON持久化的版本,让评分老师一眼看出你在用工程思维写代码。这篇笔记就围绕这个方向,从需求拆解到代码组织,把一条能落地、能交差、能演进的路线讲清楚。

2. 需求分析与技术选型:先定边界,再谈写码

2.1 需求边界:学生、管理员、系统约束,三方各要什么

很多人在大作业翻车,不是因为代码写不出来,而是需求根本没说清。选课系统第一件事不是写代码,是把角色和功能边界画出来。

学生这边至少有四个动作:浏览课程列表、按条件查课、选课、退课,还要能看到自己已经选了什么。管理员那边通常需要维护课程数据:加课、删课、调整容量。除此之外,系统自身还必须有约束逻辑:同一学生同一时间只能上一门课;每门课有容量上限,满了就不能再选;退课后名额要释放给后来的人。

把需求写成一张验收表会省很多事。表格是“大作业答辩”的好素材——老师问“你这个系统能做什么”的时候,不用临场想。

角色功能验收要点
学生浏览课程能看到课程ID、名称、时间、容量、剩余名额
学生选课重复选课被拒绝;时间冲突被拒绝;容量满被拒绝
学生退课退课后名额释放,已选列表同步更新
管理员维护课程能新增课程、删除课程、调整容量上限

这张表同时是评分点。老师说“选课系统要有业务逻辑”,指的就是表里第三行那些约束;只做增删查改,那不叫选课系统,叫课程管理台账。

2.2 技术选型:内存列表、JSON文件还是SQLite

常见做法是三种持久化方案里选一个。纯内存列表最简单,启动时手工录入数据,关程序什么都没了,适合练语法,不适合当大作业交。SQLite是最正规的,Python标准库里就有sqlite3,但新手要应付建表、游标、占位符、事务,很容易栽在API细节上,而且答辩时老师会追问索引和事务这些你答不上来的东西。

我一般会建议选JSON文件。理由很实在:标准库json模块开箱即用;数据结构直接是Python字典和列表,读进内存就是对象,不用学ORM;文件本身就是可读的,答辩时可以打开data.json给老师看数据长什么样;而且JSON是跨语言的,以后要改成Web后端,数据层不用重写。

方案持久化实现成本答辩风险
内存列表否极低老师一问就穿帮
JSON文件是低低,数据可读,好讲
SQLite是中高会被追问事务、索引、并发

2.3 项目骨架:分层设计,让评分老师一眼看出工程意识

骨架怎么搭,直接决定这份源码像不像“作业”还是像“项目”。下面这个目录结构是我反复用的一套,按数据层、业务层、界面层拆开:

course_selection/ ├── main.py # 程序入口,菜单循环 ├── data/ │ ├── courses.json # 课程数据 │ └── students.json # 学生数据 ├── models.py # 数据加载与保存(读写JSON) ├── services.py # 业务逻辑(选课、退课、冲突检查) └── requirements.txt # 依赖说明(实际上只需标准库)

models.py管“数据从哪来、存到哪去”,services.py管“选课规则怎么执行”,main.py管“用户在终端里看到什么”。三者各干各的,好处是改界面不动业务,改存储不动界面。这份骨架本身就是答辩时的加分项,老师问“如果要把数据从JSON换成数据库,要改哪几个文件”,你能直接回答“只改models.py”——这句话比十行注释都值钱。

3. 数据层与核心业务:JSON持久化与选课逻辑实现

3.1 数据模型:三个实体,每门课的时间怎么设计

先定义数据长什么样。课程、学生、选课关系,我用两个JSON文件承载:courses.json放课程,students.json放学生,学生的已选课程列表天然表达了选课关系。

courses.json 的初始结构这样定:

{ "CS101": { "name": "Python程序设计", "teacher": "张老师", "weekday": 1, "start_section": 3, "end_section": 4, "capacity": 30, "selected": 0 } }

students.json 这样定:

{ "2024001": { "name": "李小明", "password": "123456", "courses": ["CS101"] } }

课程时间用 weekday + start_section + end_section 表示。weekday取值1到5(周一到周五),节次用数字:第1节是8:00,第2节是9:50,第3节是10:40,依此类推。选课系统的时间冲突判断,本质上就是看两门课的节次区间有没有重叠,数字区间比字符串“周一3-4节”好算得多——这是我在字符串比较上翻过车之后换来的血泪经验。

3.2 JSON读写封装:load、save两个函数打天下

数据层只要两个函数:启动时load,修改后save。我把它们放在models.py里:

import json import os DATA_DIR = os.path.join(os.path.dirname(__file__), "data") def load_data(): """加载课程和学生数据,文件不存在时返回空结构""" courses_path = os.path.join(DATA_DIR, "courses.json") students_path = os.path.join(DATA_DIR, "students.json") courses = {} students = {} if os.path.exists(courses_path): with open(courses_path, "r", encoding="utf-8") as f: courses = json.load(f) if os.path.exists(students_path): with open(students_path, "r", encoding="utf-8") as f: students = json.load(f) return courses, students def save_data(courses, students): """把内存中的数据写回JSON文件,每次业务操作后调用""" with open(os.path.join(DATA_DIR, "courses.json"), "w", encoding="utf-8") as f: json.dump(courses, f, ensure_ascii=False, indent=2) with open(os.path.join(DATA_DIR, "students.json"), "w", encoding="utf-8") as f: json.dump(students, f, ensure_ascii=False, indent=2)

这里两个参数是最容易踩坑的。encoding="utf-8"保证中文不乱码,Windows上如果漏掉,写入时会用默认GBK,换台Mac读出来就是乱码。ensure_ascii=False保证json.dump输出的是可读中文而不是“\u5f20”这样的转义序列——省得答辩时打开数据文件老师以为你在加密。indent=2则是让文件格式美观,也便于在调试时直接查看。

3.3 选课与退课:业务规则全部集中在这一个文件里

services.py承担所有业务规则。选课这个动作看似简单,实际上要依次过四道检查:课程存在性、是否重复选课、容量是否已满、时间是否冲突。我把它写成独立的函数,每道检查返回一个明确的错误信息:

def select_course(students, courses, student_id, course_id): """学生选课:依次检查课程存在、重复、容量、时间冲突""" if course_id not in courses: return "课程不存在" student = students[student_id] if course_id in student["courses"]: return "你已经选过这门课了" course = courses[course_id] if course["selected"] >= course["capacity"]: return "该课程容量已满" for cid in student["courses"]: if cid not in courses: continue existing = courses[cid] if is_time_conflict(course, existing): return f"时间冲突:{cid} {existing['name']}" student["courses"].append(course_id) course["selected"] += 1 return "选课成功"

退课逻辑是选课的逆操作:把课程ID从学生列表里删掉,同时把课程selected减一。注意顺序——先改学生再改课程,最后统一save,避免只改了一半造成数据不一致。

时间冲突检测用区间重叠判断,两门课在同一天且节次区间有交集就算冲突:

def is_time_conflict(course_a, course_b): """判断两门课是否时间重叠:同一天且节次区间相交""" if course_a["weekday"] != course_b["weekday"]: return False return not ( course_a["end_section"] < course_b["start_section"] or course_b["end_section"] < course_a["start_section"] )

这段逻辑的精髓在于“不重叠”的反面。两门课不冲突只有两种可能:A在B开始之前结束,或者B在A开始之前结束;除此之外就是重叠。很多人写冲突检测时习惯枚举交集条件,写到最后自己先晕了,用“排除不重叠”的写法,四行完事。

3.4 边界与异常:容量为0、删课与孤儿数据怎么处理

边界情况在选课系统里特别多,处理原则是:防御性编程,但不要过度设计。容量为0的课直接返回“课程不存在”或者“容量已满”都可以,二选一即可,关键是行为一致。

管理员删课是个隐藏的大坑。如果直接del courses[course_id],已经选了这门课的学生的courses列表里就残留一个不存在的课程ID。这时候“已选列表”里有个幽灵课程,浏览已选课程时会报KeyError。处理策略很简单,删课时要同步清理:

def delete_course(courses, students, course_id): """删除课程,并同步清理所有学生已选列表中的残留ID""" if course_id not in courses: return "课程不存在" del courses[course_id] for student in students.values(): if course_id in student["courses"]: student["courses"].remove(course_id) return "删除成功"

容量调整同样有讲究。把容量从30改成5,而当前selected已经是20,此时selected没必要强行截断,因为selected是“已选的真实人数”,capacity只是“上限”。但选课逻辑里selected>=capacity的检查会在下次有人选课时自然生效,系统不会崩,只会提示容量已满。这种“数据冗余但自洽”的设计,在小项目里比强行约束要稳得多。

4. 界面层与系统组装:命令行菜单、逻辑编排与运行入口

4.1 命令行菜单:主循环加函数映射,别堆if-else

很多人的main.py长到三百行,全是input加if-elif,选课系统的代码量瞬间失控。正确姿势是做一个菜单分发器,把菜单序号映射到具体函数:

def show_menu(): print("\n===== 课程选课系统 =====") print("1. 浏览全部课程") print("2. 选课") print("3. 退课") print("4. 查看我的课程表") print("5. 管理员登录") print("0. 退出并保存") def main(): courses, students = load_data() while True: show_menu() choice = input("请选择操作:") if choice == "0": save_data(courses, students) print("数据已保存,再见!") break elif choice == "1": list_courses(courses) elif choice == "2": handle_select(courses, students) elif choice == "3": handle_drop(courses, students) elif choice == "4": handle_view(courses, students) elif choice == "5": handle_admin(courses, students) else: print("无效输入,请重新输入")

菜单数字和函数名一一对应,业务逻辑都在services.py里,main.py只负责“收指令、调函数、显示结果”。这样就算后续要增加“按老师查询”的功能,也只需要加一个菜单项和一个处理函数,不破坏已有代码。

4.2 输入处理与结果回显:别让一行input毁掉整个程序

命令行程序最容易翻车的地方在输入。用户随手敲了个“abc”进来,int(input())直接抛ValueError,程序当场崩掉。处理函数里要给input包一层保护:

def handle_select(courses, students): """选课功能的命令行封装:读学号、读课程ID、调业务、显示结果""" student_id = input("请输入学号:") if student_id not in students: print("学号不存在,请先注册") return course_id = input("请输入课程ID:") result = select_course(students, courses, student_id, course_id) save_data(courses, students) print(result)

每次操作后立即save_data是一个好习惯。虽然性能上有浪费,但换来的是“任何时刻崩溃都不丢数据”的确定性。演示的时候老师随手关掉终端,数据还在,这个细节很加分。result直接复用业务层返回的中文状态信息,界面层不重复判断逻辑,也不会出现“业务层说成功、界面层说失败”的不一致。

4.3 运行入口与种子数据:第一次启动就能直接演示

如果交上去的源码一启动就是个空数据库,老师第一印象就输了。我一般会在第一次启动时检测到数据缺失就自动写入样例数据,保证拿到源码就能跑、跑起来就有东西看。

import os from models import load_data, save_data def init_demo_data(courses, students): """数据文件不存在时写入种子数据,保证开箱即演示""" if courses or students: return courses["CS101"] = { "name": "Python程序设计", "teacher": "张老师", "weekday": 1, "start_section": 3, "end_section": 4, "capacity": 30, "selected": 0 } courses["MA102"] = { "name": "高等数学", "teacher": "王老师", "weekday": 3, "start_section": 1, "end_section": 2, "capacity": 50, "selected": 0 } students["2024001"] = { "name": "李小明", "password": "123456", "courses": [] } save_data(courses, students)

种子数据本身也是测试数据。CS101和MA102的时间故意错开(周一3-4节、周三1-2节),方便演示不冲突的场景;如果需要演示冲突,临时再加一门周一第4节开始的课就行。这样答辩的时候不用现场敲数据,演示节奏非常顺。

4.4 管理员入口:最小权限设计,够用就好

管理员功能不需要做成一个完整的子系统。登录(密码比对)加两个操作(新增课程、调整容量)就够支撑演示了:

ADMIN_ACCOUNT = {"admin": "admin123"} def handle_admin(courses, students): """管理员面板:验证账号后提供课程维护功能""" username = input("管理员账号:") password = input("管理员密码:") if ADMIN_ACCOUNT.get(username) != password: print("账号或密码错误") return print("1. 新增课程 2. 调整容量 3. 删除课程") op = input("请选择:") if op == "1": # 调用 add_course 函数收集课程信息并写入 pass elif op == "2": # 调用 adjust_capacity 函数 pass elif op == "3": # 调用 delete_course 函数 pass

这里我用pass占位,实际实现就是调用services.py里写好的函数。注意密码不要硬编码在源码里,这份代码要交上去,老师会看到admin123,要求不高的话可以接受,但至少别把密码写成明文“password”——这属于画蛇添足的低级错误。

5. 常见问题与避坑:现象、原因、解法

5.1 选课数据莫名丢失,重启后回到原点

现象:程序运行中一切正常,选课、退课都显示成功,但关掉终端重新启动,所有操作全部消失,数据回到刚启动时的样子。

原因:这是最典型的问题——你把选课结果只写进了内存里的字典列表,没有调用save_data。菜单循环退出时也没保存,或者save的时机不对。也有人把save写在main函数开头,加载完就save,弄反了方向。

解决:每次业务操作后立即保存,退出程序前再存一次。把save_data的调用点固定在三个地方:选课函数返回后、退课函数返回后、管理员修改课程后。宁可多存几次,不要赌程序能正常退出。

5.2 程序报KeyError:已选列表里有“幽灵课程”

现象:学生已经选了三门课,其中一门被管理员删除。学生端查看自己的课程表时,程序崩溃,报KeyError或NoneType错误。

原因:删除课程时只删了courses里的记录,没清理students里各学生的courses列表。学生在浏览已选列表时,按课程ID去courses取数据,取不到就崩了。

解决:delete_course函数里必须同步遍历所有学生清理残留ID,这一步不能省。另外在做数据展示时,也要对courses里不存在的课程ID做跳过处理,双保险:

def handle_view(courses, students): """查看我的课程表:对不存在的课程ID做防御性跳过""" student_id = input("请输入学号:") student = students.get(student_id) if not student: print("学号不存在") return print("已选课程:") for cid in student["courses"]: course = courses.get(cid) if course is None: print(f" {cid}(课程已被删除)") continue print(f" {cid} {course['name']} 周{course['weekday']} 第{course['start_section']}-{course['end_section']}节")

这里用students.get(student_id)而不是students[student_id],同样是防御性写法。输入一个不存在的学号,get返回None,程序给出友好提示而不是抛KeyError崩溃。顶层菜单里也要包一层try-except,至少保证输入非法时回到主菜单而不是退出进程,这个兜底能让演示时从容不少。

5.3 时间冲突检测失灵:周五第三节课和周一第三节课被判定冲突

现象:周五3-4节的课,和周一3-4节的课,在冲突检测里被判成了冲突。学生明明时间不重叠,却选不了课。

原因:课程时间用的是字符串结构,比如“周一 3-4节”存成了完整字符串“1-3-4”或者“周一”这个中文文本。冲突检测直接比较字符串大小,字符串比较按字符顺序,“周五”排在“周一”后面,一旦用字符串表示星期,冲突判断就退化成字典序比较,而“周一”和“周五”的字典序根本不反映真实顺序,整个区间判断全部乱套。

解决:用weekday数字加节次区间三元组来建模,冲突判断用数值区间重叠判断。这是我在这个坑里摔过之后最坚持的做法。对边界情况,还要注意节次边界相邻的情况:A课是3-4节,B课是5-6节,这两个区间不重叠,end_section=4 < start_section=5,正确判定为不冲突。如果写成了<=就会误伤相邻节次,这个等号问题属于最隐蔽的边界bug。

5.4 源码换个电脑就跑不起来:绝对路径与Python版本问题

现象:在自己机器上好好的,拷给同学或者换到实验室电脑,一跑就报FileNotFoundError或者ModuleNotFoundError。

原因:一是代码里写了绝对路径,比如D:\user\mayun\course_selection\data\courses.json,换机器必挂;二是用了机器上特有的库或者环境路径,比如pip装到用户目录、或者用了Python 3.11里才有的语法,在3.8环境直接语法错误。

解决:路径一律用os.path.join(os.path.dirname(file), "data")这种基于文件位置的相对路径。在项目根目录放requirements.txt,哪怕只需要标准库也写清楚:“本项目仅使用Python标准库(json, os),无需额外安装依赖”。这份声明能帮你避开大量环境纠纷。另外交源码前,用python -m py_compile main.py跑一遍,语法兼容性问题立刻暴露。

6. 验证与进阶:从大作业到简历项目

交作业前一定要有验证动作,不要程序能跑就默认交卷。我给自己定了一套最小验证清单,跑不通任何一条就去改代码:第一步,首次启动自动生成种子数据;第二步,用学号2024001选一门课,提示“选课成功”,关闭程序重开,数据还在;第三步,选同一门课,提示“你已经选过这门课了”;第四步,选一门与已选课程时间冲突的课,提示“时间冲突”;第五步,管理员把课程容量改成1,再用另一个学号选这门课,提示“容量已满”。这五步跑通,一份大作业的核心逻辑就稳了。

进阶方向有三条我建议你认真考虑。第一,把命令行菜单换成tkinter界面,数据层和业务层一行不用改,只写界面层,整个项目就多了一个“拥有图形界面”的标签,这在课程设计里是明确加分项。第二,把JSON文件换成SQLite,models.py里的load和save改成sqlite3的execute,你就完成了一次从文件存储到数据库的完整演进,答辩时可以回答“为什么不用数据库”这个问题。第三,给系统加一层简单的报表功能,比如统计每门课选课人数、生成退课日志,这种“有运营思维”的小功能特别容易让老师记住你的设计。

最后说一个我做课程设计时最深刻的教训。当年我的选课系统功能齐全,但数据是全内存的,答辩前一天晚上最后一次测试时手滑关了终端,数据全没了,第二天演示前在台上对着演草纸临时补数据,狼狈得不行。后来我把JSON持久化当成所有小项目的标配,不管多小的作业,只要涉及“用户输入产生数据变化”,持久化就是第一个要解决的问题。这个习惯一直带到工作里。希望这篇笔记能让你在选课系统这个题目上少走弯路,把更多精力花在真正拉开差距的工程细节上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询