1 项目总览 —— 学生信息管理系统(阶段二收官)
是什么?为什么学?
今天是阶段二收官日,没有新知识点——重点是把 Day 8~13 学的零散知识组装成一个「真正能用的」控制台管理系统。前 13 天学的知识今天全部用上:函数组织代码、列表+字典存数据、文件读写做持久化、异常处理兜底。这个系统做完,你就具备做一个「能用的工具」的能力了。
阶段知识地图:
Day 8 函数入门(def/参数/返回值/作用域) ──────┐
Day 9 函数进阶(*args/**kwargs/lambda/递归) ──┐│
Day 10 模块与包(import/标准库/自定义模块) ────┐││
Day 11 文件读写(open/with/w与a/编码) ────────┐│││
Day 12 CSV与JSON(csv模块/json四函数/持久化) ─┐││││
Day 13 异常处理(try/except/raise/自定义异常) ─┐│││││
└─┴┴┴┴┴──→ 阶段项目:学生信息管理系统
一句话串法:函数让代码模块化 → 参数让函数通用 → 作用域管好变量 → 模块让代码拆分 → 文件读写让数据落盘 → CSV/JSON 让数据有结构 → 异常处理让程序扛得住。学生管理系统就是把这些零件按顺序组装起来:函数当骨架、列表字典当仓库、JSON 当保险柜、异常处理当安全气囊。
注意事项总结
1 项目拆解表(六大功能 → 知识点):
| 功能 | 用到的知识点 | 具体写法 | 来自哪一天 |
|---|---|---|---|
| 数据模型 | 列表 + 字典 | students = []、{"name": ..., "age": ..., "score": ...} | Day 6/7 |
| 添加学生 | 函数、类型转换、异常处理 | int(input("年龄:"))、except ValueError | Day 8/2/13 |
| 查看全部 | 循环、f-string 格式化 | for s in students:、f"{s['name']:<8}..." | Day 5/9 |
| 按姓名查找 | 列表推导式、比较运算符 | [s for s in students if s["name"] == name] | Day 6/7 |
| 删除学生 | 列表推导式、切片赋值 | students[:] = [s for s in students if s["name"] != name] | Day 6 |
| 保存数据 | 函数、JSON 持久化 | json.dump(students, f, ensure_ascii=False, indent=2) | Day 8/12 |
| 加载数据 | 模块、文件判断、JSON、异常处理 | os.path.exists、json.load、except (json.JSONDecodeError, OSError) | Day 10/12/13 |
| 主菜单 | while 循环、条件判断 | while True: + if/elif + break | Day 4/5 |
2 异常防护表(风险场景 → 知识点):
| 风险场景 | 防护写法 | 知识点 |
|---|---|---|
| 年龄 / 成绩输入非数字 | try: age = int(...) + except ValueError | Day 13 异常处理 |
| 数据文件不存在(第一次运行) | if os.path.exists(DATA_FILE): | Day 10/11 模块与文件 |
| 数据文件被手动改坏 | except (json.JSONDecodeError, OSError) | Day 12/13 JSON + 异常 |
保存后中文变 \uXXXX | json.dump(..., ensure_ascii=False) | Day 12 JSON |
| 函数内重新赋值全局列表 | global students 声明 | Day 8 作用域 |
易错点
- 错误写法:一口气写完 100 行代码再运行(「憋大招」) → 问题:写了几十行没运行过一次,一运行全是错,不知道从哪改起 → 正确写法:三步走,每步都能运行、都能验证(见第 5 节)。
- 错误写法:项目做不出来就怀疑是今天的问题 → 问题:往往是前面某天基础没打牢 → 正确写法:对照拆解表自查「这一步用到了哪个知识点」,回去翻那一天的总结。
记忆口诀:函数当骨架,字典当仓库;JSON 保险柜,异常安全气囊;拆解表对号入座,小步迭代不憋大招。
2 系统设计 —— 先想清楚再动手
是什么?为什么学?
动手写代码前,先画出功能地图——这就是程序员常说的「先设计再编码」。功能越多,提前画图越重要。
注意事项总结
1 设计四要素:
- 数据模型:学生列表
students,每个学生是一个字典:{"name": 姓名, "age": 年龄, "score": 成绩} - 数据持久化:保存到
students.json,启动时自动加载 - 功能列表:添加、查看全部、按姓名查找、删除、保存、加载
- 结构组织:每个功能一个函数,
main()里用循环 + 菜单调度
2 数据格式约定:
import json
import os
DATA_FILE = "students.json"
# 一名学生 = 一个字典
# {"name": "张三", "age": 18, "score": 90}
# 所有学生 = 列表
# [{"name": "张三", ...}, {"name": "李四", ...}]
易错点
- 错误写法:不设计直接开写 → 问题:写到一半发现结构不对,返工重来 → 正确写法:先画「数据模型 + 功能列表 + 结构组织」,再写代码。
记忆口诀:先画图再动工,数据模型定基调;功能列表一条条,函数调度不乱套。
3 数据模型与作用域 —— global 的坑
是什么?为什么学?
students 是全局列表,六个函数共享它。但「修改列表内容」和「重新赋值」对 global 的要求完全不同——这是本项目最容易踩的坑。
底层原理
students.append(...) 操作的是全局列表本身(往里面塞元素),Python 能认出这是在修改已有的全局对象,不需要 global。反过来,load_data 里 students = json.load(f) 是让变量名指向一个全新的列表对象——不声明 global 的话,Python 会认为这是函数里的局部变量,赋值完函数结束就丢了,全局 students 还是空列表。
注意事项总结
1 修改列表内容 vs 重新赋值:
students = []
def add_student():
students.append({"name": "张三"}) # 修改列表内容:不需要 global
add_student()
print(students)
运行结果:
[{'name': '张三'}]
2 重新赋值必须 global:
def load_data():
global students # 必须声明!
students = json.load(f) # 重新赋值:让变量指向全新对象
3 **删除用 students[:] = ... 而不是 students = ...**:切片整体替换保留同一个列表对象,所有引用这个列表的地方看到的都是新内容;students = ... 换新对象,旧对象上的修改就丢了。在多个函数共享同一个 students 的场景下,[:] 写法更稳。
易错点
- 错误写法:
load_data里students = json.load(f)忘了global→ 问题:报UnboundLocalError,或全局 students 还是空列表 → 正确写法:函数开头写global students。 - 错误写法:删除学生用
students = [...]→ 问题:换了新对象,其它函数对旧对象的引用全部失效 → 正确写法:用students[:] = [...]原地替换。
记忆口诀:动旧对象不用 global,换新对象必须 global;删除用切片 [:],原地替换最稳当。
4 持久化与异常防护 —— JSON 保险柜 + 安全气囊
是什么?为什么学?
系统的「心脏」是保存和加载:save_data 用 json.dump 把整个学生列表存成 students.json,load_data 用 json.load 读回来。程序重启后数据还在,靠的就是这一对函数。同时,异常处理让程序在非法输入、文件损坏时也不闪退。
注意事项总结
1 保存 / 加载的核心代码:
def save_data():
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(students, f, ensure_ascii=False, indent=2)
print(f"已保存 {len(students)} 名学生到 {DATA_FILE}")
def load_data():
global students
if os.path.exists(DATA_FILE):
try:
with open(DATA_FILE, "r", encoding="utf-8") as f:
students = json.load(f)
print(f"已从 {DATA_FILE} 加载 {len(students)} 名学生")
except (json.JSONDecodeError, OSError) as e:
print(f"加载文件失败({e}),使用空数据")
else:
print("没有数据文件,从空列表开始")
2 add_student 的数字校验:
try:
age = int(input("年龄:"))
score = float(input("成绩:"))
except ValueError:
print("年龄和成绩必须是数字,添加失败")
return
int("abc") 会抛 ValueError,被 except ValueError 接住,打印友好提示——程序不崩溃,用户重新输入即可。
3 **为什么 except (json.JSONDecodeError, OSError) 而不是裸 except:**:精确捕获「文件损坏」和「读取失败」两类已知风险;裸 except: 会把所有异常(包括 Ctrl+C 的 KeyboardInterrupt)都吞掉,程序无法正常终止。
易错点
- 错误写法:保存时漏了
ensure_ascii=False→ 问题:中文变\u5f20转义码,文件没法读 → 正确写法:json.dump(..., ensure_ascii=False)。 - 错误写法:
load_data用裸except:→ 问题:Ctrl+C 也被吞掉,程序无法正常终止 → 正确写法:except (json.JSONDecodeError, OSError)。 - 错误写法:
add_student不加 try/except → 问题:用户年龄输入 “abc”,程序直接崩溃退出,之前录入的数据全白费 → 正确写法:try/except ValueError捕获并提示。
记忆口诀:json.dump 存、json.load 取,重启不丢靠闭环;非法输入加防护,文件损坏不闪退。
5 动手实践 —— 完整的学生信息管理系统
下面是整个系统,一个文件直接运行。建议先通读一遍,再照着敲一遍加深印象:
# student_manager.py —— 学生信息管理系统
import json
import os
DATA_FILE = "students.json"
students = [] # 全局数据:所有学生(每个学生是一个字典)
def load_data():
"""从文件加载学生数据,文件不存在则保持空列表"""
global students
if os.path.exists(DATA_FILE):
try:
with open(DATA_FILE, "r", encoding="utf-8") as f:
students = json.load(f)
print(f"已从 {DATA_FILE} 加载 {len(students)} 名学生")
except (json.JSONDecodeError, OSError) as e:
print(f"加载文件失败({e}),使用空数据")
else:
print("没有数据文件,从空列表开始")
def save_data():
"""把学生数据保存到文件"""
with open(DATA_FILE, "w", encoding="utf-8") as f:
json.dump(students, f, ensure_ascii=False, indent=2)
print(f"已保存 {len(students)} 名学生到 {DATA_FILE}")
def add_student():
"""添加一名学生"""
name = input("姓名:").strip()
if not name:
print("姓名不能为空")
return
try:
age = int(input("年龄:"))
score = float(input("成绩:"))
except ValueError:
print("年龄和成绩必须是数字,添加失败")
return
students.append({"name": name, "age": age, "score": score})
print(f"已添加:{name}")
def show_all():
"""查看全部学生"""
if not students:
print("暂无学生数据")
return
print(f"{'姓名':<8}{'年龄':<6}{'成绩':<8}")
for s in students:
print(f"{s['name']:<8}{s['age']:<6}{s['score']:<8}")
print(f"共 {len(students)} 人")
def find_student():
"""按姓名查找学生"""
name = input("请输入要查找的姓名:").strip()
found = [s for s in students if s["name"] == name]
if not found:
print(f"没有找到名为 {name} 的学生")
return
for s in found:
print(f"姓名:{s['name']},年龄:{s['age']},成绩:{s['score']}")
def delete_student():
"""按姓名删除学生(同名全部删除)"""
name = input("请输入要删除的姓名:").strip()
before = len(students)
students[:] = [s for s in students if s["name"] != name]
removed = before - len(students)
if removed:
print(f"已删除 {removed} 名 {name}")
else:
print(f"没有找到名为 {name} 的学生")
def main():
"""主菜单"""
load_data()
menu = (
"\n===== 学生信息管理系统 =====\n"
"1. 添加学生\n"
"2. 查看全部\n"
"3. 按姓名查找\n"
"4. 删除学生\n"
"5. 保存到文件\n"
"6. 从文件加载\n"
"0. 退出\n"
"请选择:"
)
while True:
choice = input(menu)
if choice == "1":
add_student()
elif choice == "2":
show_all()
elif choice == "3":
find_student()
elif choice == "4":
delete_student()
elif choice == "5":
save_data()
elif choice == "6":
load_data()
elif choice == "0":
save_data() # 退出前自动保存,避免数据丢失
print("再见")
break
else:
print("无效选择,请输入 0-6")
if __name__ == "__main__":
main()
运行效果:
没有数据文件,从空列表开始
===== 学生信息管理系统 =====
1. 添加学生
2. 查看全部
3. 按姓名查找
4. 删除学生
5. 保存到文件
6. 从文件加载
0. 退出
请选择:1
姓名:张三
年龄:18
成绩:90
已添加:张三
请选择:2
姓名 年龄 成绩
张三 18 90.0
共 2 人
请选择:0
已保存 2 名学生到 students.json
再见
第二次运行程序,会自动加载 students.json,刚才的学生还在。
值得注意的细节:students[:] = ... 用切片整体替换;删除功能会删掉所有同名学生;退出时自动保存 + 提供手动保存双保险;load_data 捕获 JSON 损坏不崩溃;用 if __name__ == "__main__" 保护入口,之后可以被其它模块导入复用。
6 开发流程 —— 三步搭起你的系统
是什么?为什么学?
不要一口气写完 100 行代码——新手最常见的失败方式就是「憋大招」:写了几十行没运行过一次,一运行全是错,根本不知道从哪改起。正确做法是三步走,每步都能运行、都能验证。
注意事项总结
1 第 1 步:搭骨架——先让菜单跑起来:
def add_student():
pass
def show_all():
pass
def main():
while True:
choice = input("请选择(0 退出):")
if choice == "0":
break
elif choice == "1":
add_student()
elif choice == "2":
show_all()
else:
print("无效选择")
if __name__ == "__main__":
main()
pass 是「占位语句」——函数体暂时空着也能通过语法检查。这一步的意义:先把「程序的形状」立起来,错误范围最小。
2 第 2 步:填空腹——先做「添加」和「查看」:把 add_student 和 show_all 的 pass 换成真实逻辑,跑一遍:能添加学生、能列表展示。做完这两步,系统已经有「核心价值」了。
3 第 3 步:加持久化和防护——保存/加载 + 异常处理:最后补上 save_data / load_data / find_student / delete_student,再给输入加上 try / except ValueError。这步是「锦上添花 + 加固」,即使出错也不影响前两步已经验证过的功能。
易错点
- 错误写法:一次写完所有功能再运行 → 问题:出错范围大,难以定位 → 正确写法:每完成一步就跑一次、验证一次——出问题范围小、好定位。
- 错误写法:跳过骨架直接写功能 → 问题:菜单逻辑和功能逻辑混在一起 → 正确写法:先让菜单循环跑通(用 pass 占位),再逐个填空。
记忆口诀:先骨架后填空,再加固;每步都运行,小步快迭代。
今日总结
阶段知识地图:函数 → 参数/作用域 → 模块 → 文件读写 → CSV/JSON → 异常处理,一环扣一环
系统设计先行:先画「数据模型 + 功能列表 + 结构组织」,再动手写代码
数据模型:一个列表存所有学生,一个字典(name / age / score)存一名学生
函数化:六个功能每个一个函数,main() 只做菜单调度,代码像说明书
作用域细节:students.append(...) 不用 global,students = ... 必须 global
持久化闭环:json.dump 保存 + json.load 加载,重启程序数据还在
异常防护:输入非数字(except ValueError)、文件损坏(except (JSONDecodeError, OSError))都不闪退
项目拆解能力:能对照「功能 → 知识点」表格,说清每个功能用到了哪几天的知识
开发流程:三步走(骨架 → 填空腹 → 加固),每步都能运行验证
独立完成学生信息管理系统,并测试非法输入、文件损坏等边界场景
| 报错 | 原因 | 修复 |
|---|---|---|
json.JSONDecodeError | students.json 被手动改坏或格式不对 | 删除损坏文件重新开始,或在 load_data 中捕获 |
UnboundLocalError | 函数内给全局列表赋值却忘了 global 声明 | 用 students[:] = ... 修改,或在函数开头声明 global |
ValueError: invalid literal for int() | 年龄输入了非数字 | 参考 add_student 用 try / except 捕获 |
KeyError: 'score' | 字典里没有这个键(比如旧数据格式不同) | 打印 students 检查每项的键是否一致 |
保存后中文变 \uXXXX | 写 JSON 时漏了 ensure_ascii=False | 保存时加上 ensure_ascii=False |