AI编程(十七):成绩管理器 — 列表+字典的组合拳

作者:

增删改查、统计平均分、排名、及格率、导出文件——数据管理程序的标准套路

一、场景:期末了,成绩单一团乱

假设你是老师(或者家长,要管理孩子的各科成绩):

  • 语文85,数学92,英语78——记在纸上
  • 想算全班平均分?拿计算器一个个加
  • 想知道谁进步了?翻上次的记录
  • 领导要一份”及格率统计表”?重新算一遍再誊抄

一件纯机械的事,耗掉一晚上。

今天我们做一个成绩管理器:录入成绩、查看名单、算平均分、找最高最低、排名、算及格率、导出成文件。

你可能觉得这是”数据库程序”,很高大上。其实就是前面所有知识的组合拳:

  • 字典存一个学生的成绩(姓名→分数的对应)
  • 列表存全班学生(一堆字典排好队)
  • 循环+条件做菜单和增删改查
  • 文件操作让数据关了程序也不丢

学会它,你就会发现:通讯录、库存管理、记账本,全是同一个套路,换个字段名而已。

二、先搞懂数据结构

一个学生的成绩,用字典表示:

{"姓名": "小明", "语文": 85, "数学": 92, "英语": 78}

全班学生,用列表装起来:

students = [
    {"姓名": "小明", "语文": 85, "数学": 92, "英语": 78},
    {"姓名": "小红", "语文": 90, "数学": 88, "英语": 95},
    {"姓名": "小刚", "语文": 60, "数学": 55, "英语": 70},
]

“列表套字典”——这是Python里最常用的数据组织方式,没有之一。字典描述”一个东西的各项属性”,列表把”一堆东西”排好队。以后你处理任何”一行一行的表格数据”,都是这个结构。

怎么存到文件里?

第12篇学过文件读写,但列表和字典不能直接f.write()。这里要用一个新朋友:json模块。

json是Python自带的工具箱,专门把列表/字典变成字符串(存进文件),以及把字符串变回列表/字典(读出来)。就像把乐高城堡拆成扁平的积木块装箱(存),要用时再拼回来(读)。

用法就两个函数:

import json

# 存:把列表变成JSON字符串写进文件
json.dump(students, f, ensure_ascii=False, indent=2)

# 读:把文件里的JSON字符串变回列表
students = json.load(f)
  • ensure_ascii=False:不加它,中文会变成u5c0fu660e这种乱码
  • indent=2:让文件内容带缩进,好看(方便你打开检查)

三、先跟AI聊

你的提示词:

用Python写一个成绩管理器。用列表存学生数据,每个学生是字典(姓名、语文、数学、英语)。有菜单循环:1录入成绩 2查看全部 3统计信息 4退出。数据用json保存到文件,程序启动时自动加载。加上详细的中文注释。

AI会给你一段代码,大概长这样:

import json

FILENAME = "scores.json"


def load_scores():
    """从文件加载成绩数据"""
    try:
        with open(FILENAME, "r", encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        return []  # 文件不存在就返回空列表


def save_scores(students):
    """把成绩数据保存到文件"""
    with open(FILENAME, "w", encoding="utf-8") as f:
        json.dump(students, f, ensure_ascii=False, indent=2)


def show_all(students):
    """显示所有学生的成绩"""
    if len(students) == 0:
        print("还没有成绩记录")
        return
    print(f"n{'姓名':6}{'数学':>6}{'英语':>6}{'总分':>8}{'平均':>8}")
    print("-" * 48)
    for s in students:
        total = s["语文"] + s["数学"] + s["英语"]
        avg = total / 3
        print(f"{s['姓名']:6}{s['数学']:>6}{s['英语']:>6}{total:>8}{avg:>8.1f}")


# === 主程序 ===
students = load_scores()  # 启动时加载已有数据
print("=== 成绩管理器 ===")

while True:
    print("n1. 录入成绩  2. 查看全部  3. 退出")
    choice = input("请选择:")

    if choice == "1":
        name = input("姓名:")
        chinese = int(input("语文成绩:"))
        math = int(input("数学成绩:"))
        english = int(input("英语成绩:"))
        # 组装成字典,加进列表
        students.append({
            "姓名": name,
            "语文": chinese,
            "数学": math,
            "英语": english,
        })
        save_scores(students)  # 立即保存
        print(f"已录入 {name} 的成绩")

    elif choice == "2":
        show_all(students)

    elif choice == "3":
        print("再见!")
        break

    else:
        print("无效选择,请输入1-3")

四、逐行理解

FILENAME = "scores.json" —— 大写常量

文件名只在一个地方定义,后面全用变量。哪天想改文件名,只改一行。用大写是程序员的习惯,表示”这个值不会变”。

load_scores():第一次见try/except这样用

try:
    with open(FILENAME, "r", encoding="utf-8") as f:
        return json.load(f)
except FileNotFoundError:
    return []

第一次运行程序时,文件还不存在,直接打开会崩。所以用try...except兜底:文件不存在不是错误,只是”还没有数据”,返回空列表就好。

这个思路很常见:把”预期内的意外”用except转成正常逻辑。

save_scores():两行搞定持久化

json.dump(students, f, ensure_ascii=False, indent=2)

json.dump(数据, 文件):把数据变成JSON格式写进文件。存完后scores.json长这样:

[
  {
    "姓名": "小明",
    "语文": 85,
    "数学": 92,
    "英语": 78
  },
  {
    "姓名": "小红",
    "语文": 90,
    "数学": 88,
    "英语": 95
  }
]

每次录入后立即save_scores(),这样程序意外退出数据也不丢。这叫”写时保存”。

show_all():表格打印

print(f"{s['姓名']:6}{s['数学']:>6}{s['英语']:>6}{total:>8}{avg:>8.1f}")

第15篇学过的对齐魔法又来了:<10左对齐占10位,>6右对齐占6位。{avg:>8.1f}里的.1f表示保留1位小数(f是float格式化)。

s['姓名']——注意字典的key是中文字符串,用引号包起来。

菜单循环

while True:
    choice = input("请选择:")
    if choice == "1":
        ...
    elif choice == "3":
        break

标准的菜单套路:循环显示选项 → 输入数字 → 对应分支干活 → 循环回来。程序不退出,数据一直”热”在内存里。

五、运行试试

=== 成绩管理器 ===

1. 录入成绩  2. 查看全部  3. 退出
请选择:1
姓名:小明
语文成绩:85
数学成绩:92
英语成绩:78
已录入 小明 的成绩

1. 录入成绩  2. 查看全部  3. 退出
请选择:2

姓名            语文    数学    英语      总分      平均
------------------------------------------------
小明            85    92    78     255    85.0

录入三个人后退出程序,再重新运行,选2查看——数据还在!因为是从scores.json加载的。这就是”持久化”:数据的生命比程序长。

六、第一个改进:统计信息(平均/最高/最低)

加一个菜单选项3″统计信息”,原来的退出改成4:

def stats(students):
    """统计全班平均分、最高分、最低分"""
    if len(students) == 0:
        print("还没有成绩记录")
        return

    # 先算出每个人的总分
    totals = []
    for s in students:
        totals.append(s["语文"] + s["数学"] + s["英语"])

    # 全班平均分 = 所有总分之和 ÷ 人数 ÷ 3科
    avg_all = sum(totals) / len(totals) / 3
    print(f"n全班平均分:{avg_all:.1f}")

    # 找最高分和最低分(记录下标)
    best_i = 0
    worst_i = 0
    for i in range(len(totals)):
        if totals[i] > totals[best_i]:
            best_i = i
        if totals[i] < totals[worst_i]:
            worst_i = i

    print(f"最高分:{students[best_i]['姓名']}(总分{totals[best_i]})")
    print(f"最低分:{students[worst_i]['姓名']}(总分{totals[worst_i]})")

几个值得学的套路

totals列表:先用循环把每个人的总分算出来单独存一个列表。因为总分要用三次(平均、找最高、找最低),提前算好不重复劳动。

sum():求和内置函数,sum([255, 273, 185]) = 713。不用自己写循环累加。

找最大最小的”记录下标”法

best_i = 0
for i in range(len(totals)):
    if totals[i] > totals[best_i]:
        best_i = i

假设第0个是最高,然后挨个比,谁更高就更新”最高的是谁”。注意存的是下标(位置)不是分数本身——因为我们最后要显示的是名字,得靠下标回students里查。

(Python其实有内置的max()函数,max(totals)直接给最大值。但用它只能拿到分数,拿名字还得再找一遍。自己写循环反而更顺手——工具顺手比工具高级更重要。)

输出效果

请选择:3

全班平均分:79.2
最高分:小红(总分273)
最低分:小刚(总分185)

七、第二个改进:按成绩排名

考试完最想知道的就是排名。用第15篇学过的sorted()

def show_ranking(students):
    """按总分从高到低排名"""
    if len(students) == 0:
        print("还没有成绩记录")
        return

    # sorted不会改动原列表,返回新列表
    ranked = sorted(students,
                    key=lambda s: s["语文"] + s["数学"] + s["英语"],
                    reverse=True)

    print("n🏆 成绩排名:")
    for i, s in enumerate(ranked):
        total = s["语文"] + s["数学"] + s["英语"]
        # 给前三名加个标记
        medal = ""
        if i == 0:
            medal = "🥇"
        elif i == 1:
            medal = "🥈"
        elif i == 2:
            medal = "🥉"
        print(f"第{i + 1}名 {medal} {s['姓名']:<8} 总分:{total}")

sorted vs sort

第15篇用过items.sort(...)。这里用sorted(students, ...),区别是:

  • 列表.sort():原地排,列表自己变了
  • sorted(列表)返回一个新列表,原列表不动

成绩排名只是”看一眼”,不该打乱内存里的原始顺序(万一以后要按录入顺序显示呢),所以用sorted更安全。

lambda这次长什么样

key=lambda s: s["语文"] + s["数学"] + s["英语"]

lambda s: ...的意思是”给我一个学生s,我返回他的总分”。按总分排,reverse=True从高到低。

前三名奖牌

if i == 0:
    medal = "🥇"
elif i == 1:
    medal = "🥈"
elif i == 2:
    medal = "🥉"

纯粹是让输出好看的小彩蛋。emoji可以直接写在字符串里,print出来就是表情。

🏆 成绩排名:
第1名 🥇 小红      总分:273
第2名 🥈 小明      总分:255
第3名 🥉 小刚      总分:185

八、第三个改进:及格率统计 + 导出到文件

及格率

def pass_rate(students, pass_line=60):
    """统计三科全部及格的学生比例"""
    if len(students) == 0:
        print("还没有成绩记录")
        return

    passed = 0
    for s in students:
        # 三科都>=60才算及格(注意是and不是or)
        if s["语文"] >= pass_line and s["数学"] >= pass_line and s["英语"] >= pass_line:
            passed = passed + 1
            print(f"  ✓ {s['姓名']}(三科全部及格)")
        else:
            # 找出哪科没及格
            failed = []
            if s["语文"] < pass_line:
                failed.append("语文")
            if s["数学"] < pass_line:
                failed.append("数学")
            if s["英语"] < pass_line:
                failed.append("英语")
            print(f"  ✗ {s['姓名']}({'、'.join(failed)}未及格)")

    rate = passed / len(students) * 100
    print(f"n及格率:{passed}/{len(students)} = {rate:.0f}%")

两个亮点:

and连条件:三科及格才算——第五篇学的逻辑运算符。如果写成or(任一科及格就算),及格率会高得离谱,逻辑就错了。

'、'.join(failed):join是split的反操作!'、'.join(["语文", "数学"])"语文、数学"。把列表拼成字符串。第15篇split拆词,今天join合词,一对好搭档。

导出到文件(csv格式)

def export_to_file(students, filename="成绩单.csv"):
    """把成绩导出成csv文件,可以用Excel打开"""
    with open(filename, "w", encoding="utf-8-sig") as f:
        # 先写表头
        f.write("姓名,语文,数学,英语,总分,平均分n")
        # 一行写一个学生
        for s in students:
            total = s["语文"] + s["数学"] + s["英语"]
            avg = total / 3
            f.write(f"{s['姓名']},{s['语文']},{s['数学']},{s['英语']},{total},{avg:.1f}n")
    print(f"已导出到 {filename}(可用Excel打开)")

csv格式:字段用逗号分隔的纯文本,Excel认识它。双击csv文件就打开一张表格——领导要的”成绩单”直接发过去。

  • encoding="utf-8-sig":比普通utf-8多存一个BOM标记,Excel打开中文csv不乱码的关键(Excel默认认GBK,有了BOM它就知道是UTF-8)。这个细节不知道就会踩坑,现在你知道了。
  • 表头单独写一行,数据一行一个学生,n换行。
姓名,语文,数学,英语,总分,平均分
小明,85,92,78,255,85.0
小红,90,88,95,273,91.0
小刚,60,55,70,185,61.7

完整菜单

把所有功能接进主循环:

students = load_scores()
print("=== 成绩管理器 ===")

while True:
    print("n1. 录入成绩  2. 查看全部  3. 统计信息")
    print("4. 成绩排名  5. 及格率    6. 导出文件  7. 退出")
    choice = input("请选择:")

    if choice == "1":
        name = input("姓名:")
        try:
            chinese = int(input("语文成绩:"))
            math = int(input("数学成绩:"))
            english = int(input("英语成绩:"))
        except ValueError:
            print("成绩必须是数字!")
            continue
        students.append({"姓名": name, "语文": chinese,
                         "数学": math, "英语": english})
        save_scores(students)
        print(f"已录入 {name} 的成绩")

    elif choice == "2":
        show_all(students)
    elif choice == "3":
        stats(students)
    elif choice == "4":
        show_ranking(students)
    elif choice == "5":
        pass_rate(students)
    elif choice == "6":
        export_to_file(students)
    elif choice == "7":
        print("再见!")
        break
    else:
        print("无效选择,请输入1-7")

一个完整的”数据管理程序”成型了。每个功能一个函数,主循环只负责调度——菜单程序的黄金结构

九、回顾一下用到了什么

知识点 在哪里用的

|——–|———–|

字典 每个学生的成绩数据
列表 全班学生的集合
列表套字典 整个数据结构
json模块 数据存取(dump/load)
try/except 文件不存在、输入非数字
while True菜单 主循环
append 添加学生
sum/len 求和、计数
记录下标法 找最高分最低分
sorted+lambda 按总分排名
and条件 三科全部及格判断
join 把科目列表拼成字符串
文件写入 导出csv
f-string格式化 对齐、保留小数

14个知识点,全部学过。 你没学”数据库”、没学”类和对象”,但做出的东西和很多真实的成绩管理系统,核心逻辑是一回事。

十、今日小练习

试着自己加一个功能(可以问AI帮忙):

功能:删除和修改学生记录

菜单加两个选项:

  • “删除学生”:输入姓名,找到后从列表里删掉,保存
  • “修改成绩”:输入姓名,找到后重新录入成绩,保存

提示:你需要列表的remove()方法,或者”找到下标再pop()“。找人的循环你已经会写了。

想想一个边界情况:输入的姓名不存在怎么办?程序该说什么?(提示:循环找完没找到,就打印”查无此人”。)


下一篇:待办事项工具 — 你自己的第一个”正经软件”:添加任务、标记完成、按优先级排序、未完成数量提醒。它是本系列的收官项目,会用到你学过的所有东西。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注