第20章-JSON与综合项目:学生信息管理系统


这周你已经攒下三件工具:函数(打包代码)、文件(存住数据)、异常(防崩溃)。今天再补最后一个零件——JSON,然后把四样东西拼起来,做本周的终极项目:学生信息管理系统。

先交代一下 JSON 是什么。说白了,它就是一套全世界程序都认的"数据包装纸":Python 的字典和列表,用 JSON 写进文件,别的地方(网页、其他语言写的程序)也能读。更妙的是它长得几乎和 Python 的字典/列表一模一样,你几乎不用学新东西。学完 JSON,今天剩下的时间都交给这个系统:能添加学生(姓名+成绩)、查看全部、退出,数据存进 JSON 文件,程序关了再开数据还在。

这个系统把本周所有知识串成一条线:函数组织功能、JSON 存数据、异常兜底、while 循环做菜单。它还是第 6 周大项目(通讯录、记账本)的雏形,今天吃透它,后面就轻松了。

JSON:通用数据包装纸

JSON(读作"杰森",全称 JavaScript Object Notation)是一种文本数据格式,用来在不同程序之间传数据、存数据。它的两个基本结构:

  • { } 对象 = 对应 Python 的字典(键值对)。
  • [ ] 数组 = 对应 Python 的列表

写起来几乎和 Python 一模一样,区别只有几个小地方(比如 JSON 里的字符串必须用双引号)。看一段 JSON:

{
  "name": "小明",
  "age": 18,
  "scores": [90, 85, 92]
}

一个对象(字典),里面有三个键,第三个键的值还是一个数组(列表)。和 Python 的字典对比一下:

data = {"name": "小明", "age": 18, "scores": [90, 85, 92]}
print(data)

输出:

{'name': '小明', 'age': 18, 'scores': [90, 85, 92]}

Python 打印时键用的是单引号,JSON 里必须双引号。除此之外,结构完全一样。所以你可以把 JSON 理解成"字典和列表的通用文本版本"。一句话:JSON 就是"用文本描述数据"的标准写法,全世界程序都认。

三个容易踩的点:

  • JSON 里字符串必须双引号:写 {'name': '小明'} 是 Python 字典,不是合法 JSON;"name": "小明" 才是。
  • JSON 没有单引号、没有注释:它是纯数据,别在里面写 # 注释。
  • JSON 不能存 Python 专属类型:比如 datetimeset 直接存会报错,存之前转成字符串/列表(今天用不到,先知道有这个坑)。

dumps 和 loads:内存里的双向转换

json 模块(也是标准库)提供两对函数:

  • json.dumps(数据):把 Python 数据(字典/列表)变成 JSON 字符串。dumps 里的 "s" 可以记成 "string"(变成字符串)。
  • json.loads(字符串):把 JSON 字符串变回 Python 数据,反着来。

两个常用参数,都写在 dumps / dump 里:

  • ensure_ascii=False中文正常显示。不写的话中文会变成 \u660e 这种转义码,人看不了。
  • indent=2格式化排版,让 JSON 换行缩进好读。不写就挤成一行(也能存,就是难读)。
import json

data = {"name": "小明", "age": 18, "scores": [90, 85, 92]}

# dumps:Python 数据 → JSON 字符串
s = json.dumps(data, ensure_ascii=False, indent=2)
print("转换后的 JSON 字符串:")
print(s)

# loads:JSON 字符串 → Python 数据
data2 = json.loads(s)
print("转换回来的数据:")
print(data2["name"], data2["scores"])

输出:

转换后的 JSON 字符串:
{
  "name": "小明",
  "age": 18,
  "scores": [
    90,
    85,
    92
  ]
}
转换回来的数据:
小明 [90, 85, 92]

ensure_ascii 的效果看一眼就懂,为什么必须加它:

import json

data = {"name": "小明"}
print(json.dumps(data))                        # 不写 ensure_ascii:中文变转义码
print(json.dumps(data, ensure_ascii=False))    # 写了:中文正常显示

输出:

{"name": "\u660e"}
{"name": "小明"}

(顺带说一句,术语上"变成字符串"叫序列化,"变回数据"叫反序列化,知道就行,以后看文档会碰到。)

三个注意点:

  • ensure_ascii=False 必须写在 dumps / dump 里:不写中文就变 \u660e,人是看不懂的。
  • dumps 返回字符串,dump 写文件:一个字母的差别——函数名带 "s" 的返回字符串,不带 "s" 的操作文件,别混。
  • json.loads 的输入必须是合法 JSON:字符串里语法错了(比如单引号)会报 JSONDecodeError

dump 和 load:直接和文件打交道

上面是"内存 ↔ 字符串",实际项目更常用的是"内存 ↔ 文件":

  • json.dump(数据, 文件对象):把数据直接写进文件(注意:dumps 的 s 没了)。
  • json.load(文件对象):从文件直接读出数据。

配合 with open(...)encoding="utf-8" 使用(第 18 章的规矩不变)。这就是数据持久化的完整版:程序关掉,数据躺在 JSON 文件里;下次运行,load 回来接着用。今天项目核心就是这一对函数。

import json

data = {"name": "小明", "age": 18, "scores": [90, 85, 92]}

# dump:写进文件(记住:dump 后面接文件对象)
with open("student.json", "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

print("已经写入 student.json,去文件夹里打开看看内容")

# load:从文件读出来
with open("student.json", "r", encoding="utf-8") as f:
    loaded = json.load(f)

print("读回来的数据:", loaded)
print("名字:", loaded["name"])
print("年龄:", loaded["age"])

输出:

已经写入 student.json,去文件夹里打开看看内容
读回来的数据: {'name': '小明', 'age': 18, 'scores': [90, 85, 92]}
名字: 小明
年龄: 18

跑完后用记事本打开 student.json,你会看到格式化好的 JSON——这就是程序下一次运行要读的"记忆"。

三个细节:

  • dump(数据, f) 两个参数顺序别反:先数据、后文件对象;反了报 AttributeError
  • 写文件照样带 encoding="utf-8":json 模块不会替你解决编码,漏了中文照样乱码。
  • 文件里是"坏 JSON"时 load 报错:用记事本手动乱改过 JSON(比如漏逗号)就会 JSONDecodeError。别手动乱改,要改就用程序改。

综合项目:学生信息管理系统

现在做本周的终极项目。需求很简单,三条:

  1. 能添加学生(输入姓名和成绩)
  2. 能查看全部学生
  3. 能退出,数据存进 JSON 文件,程序关了再开还在

动手前先把设计想清楚,三个关键点:

  1. 数据长什么样:一个字典列表 [{"name": "小明", "score": 92}, ...]——就是第 3 周学的"列表套字典"。
  2. 每个功能一个函数:加载、保存、添加、查看、主菜单各写一个函数(第 16 章学的用上了)。
  3. 数据从哪来到哪去:程序启动时 load 文件 → 内存里改 → 每次添加完立刻 save → 退出后文件里有最新数据。

完整代码:

import json

FILE = "students.json"    # 数据存哪个文件,集中写在开头好改

# 1. 加载:从文件读学生列表;文件不存在就返回空列表
def load_students():
    try:
        with open(FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        return []          # 第一次运行文件不存在,就当没有学生

# 2. 保存:把学生列表写回文件
def save_students(students):
    with open(FILE, "w", encoding="utf-8") as f:
        json.dump(students, f, ensure_ascii=False, indent=2)

# 3. 添加学生
def add_student(students):
    name = input("姓名:")
    score = int(input("成绩:"))
    students.append({"name": name, "score": score})
    save_students(students)          # 立刻存盘
    print("添加成功!")

# 4. 查看全部学生
def show_students(students):
    if not students:
        print("还没有学生,先添加一个吧")
        return
    for i, stu in enumerate(students, 1):   # enumerate 自带编号
        print(f"{i}. {stu['name']}{stu['score']}分")

# 5. 主菜单:循环显示菜单,按数字选功能
def main():
    students = load_students()       # 启动时把数据读进内存
    while True:
        print("\n1.添加学生  2.查看学生  3.退出")
        choice = input("请选择:")
        if choice == "1":
            add_student(students)
        elif choice == "2":
            show_students(students)
        elif choice == "3":
            print("再见!")
            break
        else:
            print("输入无效,重新选择")

main()

完整跑一遍:

1.添加学生  2.查看学生  3.退出
请选择: 1
姓名: 小明
成绩: 92
添加成功!

1.添加学生  2.查看学生  3.退出
请选择: 2
1. 小明:92分

1.添加学生  2.查看学生  3.退出
请选择: 3
再见!

验证数据持久化:再运行一次程序,选"2.查看学生"——小明还在!因为他被存进了 students.json。这就是 JSON + 文件带来的"记忆"。

代码里三个容易写错的地方:

  • 引号stu['name'] 用单引号还是双引号都行,都是字符串;但注意外层是 f-string 时,里面别套同一种引号,如 f"{stu['name']}" 是对的。
  • 每次添加后必须 save_students:只改内存不存盘,程序一关就白干。保存逻辑写在"数据变化"之后。
  • 主菜单输入是字符串choice 是字符串,判断用 "1""2" 而不是数字 1;输入成绩才需要 int() 转换。

随堂练习

  1. 练习一(跑通 + 验证,必做):运行上面的系统:添加 2 个学生 → 退出 → 重新运行 → 选择"查看",确认数据还在。 提示:跑完后打开项目文件夹里的 students.json,看看数据长什么样;故意删掉这个文件再运行一次,观察"文件不存在"被哪个函数接住了。

  2. 练习二(改成记账本):把系统改造成"记账本":能记录收支(金额、用途),能查看总收入和总支出。 提示:每条记录存成 {"kind": "收入"/"支出", "amount": 金额, "note": "用途"};查看时用循环累加收入和支出;存文件换成 records.json。卡住先问 AI,别自己硬扛。

  3. 进阶题(给"添加学生"加异常处理):用户输入成绩时输入了文字(比如 abc),程序会崩,给它加上 try/except 让程序不崩溃,并提示"成绩必须是数字"。 提示:score = int(input("成绩:")) 这一行放进 tryexcept ValueError 提示重输;结合周四学的 while True 做到"输错就重来"。

  4. 进阶题(删除学生):给系统加一个功能:选"4.删除学生",输入序号删除对应学生,删除后保存。 提示:show_students 先带编号展示;用 students.pop(序号 - 1) 删除(因为编号从 1 开始,下标从 0 开始);删完记得 save_students

卡住了?把需求丢给 AI

场景:让 AI 帮忙写项目

提示词:
请帮我把这个需求写成 Python 程序:一个备忘录,支持添加、查看、
删除事项,数据保存到 memo.json 文件。用函数封装每个功能,
加注释。我是初学者,代码尽量简单。

请按这个格式写:先写每个函数是干什么的,再写主循环,
最后写怎么运行测试。
场景:改造成记账本

提示词:
我有一份"学生信息管理系统"的代码,用 JSON 文件存数据。
我想把它改造成"记账本":能记录收支(金额、用途),
能查看总收入和总支出。请告诉我需要改哪几个函数、
数据结构怎么设计,先给思路再给代码,我是初学者。
场景:检查代码 / 调试

提示词:
下面是我的代码,运行时报错:ValueError: invalid literal for
int() with base 10: 'abc'。请帮我看代码哪里有问题,并告诉我
怎么加异常处理让程序不崩溃。代码:{粘贴你的代码}。我是初学者,
请解释清楚原因。

常见报错速查

报错信息 什么意思 怎么办
JSONDecodeError: Expecting value JSON 文件内容坏了(多半是被手动乱改过) 别用记事本乱改 JSON;检查逗号、引号;实在坏了删除文件重新生成
json.dump()AttributeError dump 的两个参数顺序写反了 顺序是 json.dump(数据, 文件对象)
中文存进文件变成 \u660e 忘了写 ensure_ascii=False dumps / dump 里都加 ensure_ascii=False
ValueError: invalid literal for int() 把非数字文字转成 int try/except ValueError 兜住(进阶题就是练这个)
KeyError: 'name' 字典里没有这个键 检查 JSON 数据结构和取的键名是否一致;用 get() 代替 [] 更稳
重启程序数据丢了 忘了调 save_students / json.dump 每次数据变化后立刻保存到文件

收个尾,以及周六的周测

本周的内容到今天结束。把这一周串起来看:函数把代码打包成模块,异常让程序扛得住意外,JSON + 文件让数据活过重启,学生管理系统就是把这四样装进一个程序里。你能跑通它、能把它改成记账本、能往里加功能,第 6 周的大项目就是它的加强版——那会儿你就不是照抄,而是自己设计了。

本周六进行第 4 周周测,先把规矩说清楚:

  • 形式:上机操作题(约 40 分钟,在电脑上实际操作编程),包含 5 道题:
  • 写一个函数并调用(带参数和返回值)
  • 用 random 生成随机数(如掷骰子)
  • 文件写入 + 追加 + 读取(记得 encoding="utf-8"
  • 用 try/except 处理除以 0 的异常
  • JSON 读写(用 json.dump / json.load 存读数据)

  • 考核要点:以操作为主、理论为辅,每道题都是一段能独立完成的小程序;重点看你能不能自己写出代码,而不是背概念。

  • 通过标准与补测:完成 60% 即通过;未通过者周日到教室补测,补测重点为周测中做错的题目部分。

  • 备战建议:周六周测的 5 道题,基本都出自本周每天的随堂练习。把每天的练习一、练习二重新各写一遍,能做到"不看教材独立写出来",周测就稳了。

周五晚自习把这份复习清单逐条自查,哪项打不了勾,就翻对应章节补一补,再让 AI 出几道题练手:

  • [ ] 能独立写出 def 函数,带参数和返回值
  • [ ] 知道怎么导入并使用 random / math / datetime
  • [ ] 会用 with open 读写文件,记得写 encoding="utf-8"
  • [ ] 会用 try / except 兜住常见错误
  • [ ] 会用 json.dump / json.load 存读数据
  • [ ] 学生信息管理系统能跑通

下周进入第 5 周:面向对象(class)与 AI 提问技巧。今天这个"系统 = 函数 + 数据"的写法,下周会升级成"类"的写法——把数据和操作它的函数打包在一起,那是更大一号的"收纳盒"。周测前先把本周练熟,下周一见。