Python综合项目实战:学生信息管理系统函数版与面向对象版

19 篇过去了。你学了变量、容器、函数、类、文件、异常、模块……如果你是一路跟着敲代码过来的,现在已经具备了独立写一个完整项目的能力。问题是——从哪开始?

这一篇就是答案。我们将从零开始,构建一个完整的学生信息管理系统。它不是什么玩具 demo——它有菜单、增删改查、数据持久化、输入校验,是一个拿出去能给朋友演示的完整程序。

我们会先写一个函数式的版本(适合理解程序流程),再用面向对象的方式重构(适合维护和扩展)。读完这一篇,你就拥有了从"能写代码"到"能写项目"的桥梁。

一、项目设计

需求分析——想清楚再动手

别急着写代码。先花 5 分钟想清楚这个系统要做什么:

功能描述
添加学生输入学号、姓名、成绩,存入系统
显示全部以表格形式列出所有学生信息
按学号查找输入学号,找到并显示该学生
修改成绩输入学号和新成绩,更新数据
删除学生输入学号,从系统中移除
保存数据将所有学生数据写入 JSON 文件
启动读取程序启动时自动从文件加载已有数据
菜单循环用户可以反复操作,输入 0 退出

需求明确之后,项目的整体架构自然浮现:一个主菜单循环、几个功能函数/方法、一个数据存储(JSON 文件)、输入校验辅助函数。
在这里插入图片描述

图20-1 系统架构:菜单界面 → 业务逻辑(增删改查)→ 数据层(列表/字典)→ 持久层(JSON 文件)。

二、编码实现

版本一:函数式实现

函数式版本适合初学者快速理解"项目是怎么搭起来的"。核心思路:用一个字典列表存储所有学生,每个功能写成一个函数。

import json
from pathlib import Path

DATA_FILE = Path("students_v1.json")

# ========== 数据层 ==========

def load_students():
    """从 JSON 文件加载学生数据"""
    if not DATA_FILE.exists():
        return []
    try:
        return json.loads(DATA_FILE.read_text(encoding="utf-8"))
    except (OSError, json.JSONDecodeError):
        return []

def save_students(students):
    """保存学生数据到 JSON 文件"""
    DATA_FILE.write_text(
        json.dumps(students, ensure_ascii=False, indent=2),
        encoding="utf-8"
    )
    print("数据已保存。")

# ========== 输入校验 ==========

def read_int(prompt):
    """读入整数,带校验"""
    while True:
        try:
            return int(input(prompt))
        except ValueError:
            print("请输入有效的整数。")

def read_float(prompt):
    """读入浮点数,带校验"""
    while True:
        try:
            return float(input(prompt))
        except ValueError:
            print("请输入有效的数字。")

# ========== 业务逻辑 ==========

def add_student(students):
    """添加一个学生"""
    student_id = read_int("学号:")
    # 检查重复
    for s in students:
        if s["student_id"] == student_id:
            print(f"学号 {student_id} 已存在,添加失败。")
            return
    name = input("姓名:").strip()
    if not name:
        print("姓名不能为空。")
        return
    score = read_float("成绩:")
    if not (0 <= score <= 100):
        print("成绩应在 0~100 之间。")
        return
    students.append({"student_id": student_id, "name": name, "score": score})
    print(f"已添加:{name}")

def show_all(students):
    """显示所有学生"""
    if not students:
        print("暂无学生数据。")
        return
    print(f"\n{'学号':<10}{'姓名':<10}{'成绩':<8}{'等级'}")
    print("-" * 36)
    for s in students:
        level = get_level(s["score"])
        print(f"{s['student_id']:<10}{s['name']:<10}{s['score']:<8.1f}{level}")

def get_level(score):
    if score >= 90: return "A"
    elif score >= 80: return "B"
    elif score >= 70: return "C"
    elif score >= 60: return "D"
    return "F"

def search_student(students):
    """按学号查找"""
    sid = read_int("请输入要查找的学号:")
    for s in students:
        if s["student_id"] == sid:
            print(f"学号:{s['student_id']}  姓名:{s['name']}  成绩:{s['score']:.1f}")
            return s
    print("未找到该学号的学生。")
    return None

def modify_score(students):
    """修改成绩"""
    sid = read_int("请输入要修改成绩的学号:")
    for s in students:
        if s["student_id"] == sid:
            new_score = read_float("新成绩:")
            if not (0 <= new_score <= 100):
                print("成绩应在 0~100 之间。")
                return
            s["score"] = new_score
            print(f"{s['name']} 的成绩已更新为 {new_score}")
            return
    print("未找到该学号的学生。")

def delete_student(students):
    """删除学生"""
    sid = read_int("请输入要删除的学号:")
    for i, s in enumerate(students):
        if s["student_id"] == sid:
            name = students.pop(i)["name"]
            print(f"已删除:{name}")
            return
    print("未找到该学号的学生。")

def show_statistics(students):
    """统计分析"""
    if not students:
        print("暂无数据。")
        return
    scores = [s["score"] for s in students]
    avg = sum(scores) / len(scores)
    passed = sum(1 for s in scores if s >= 60)
    print(f"\n===== 统计 =====")
    print(f"总人数:{len(students)}")
    print(f"平均分:{avg:.1f}")
    print(f"最高分:{max(scores)}")
    print(f"最低分:{min(scores)}")
    print(f"及格率:{passed/len(scores)*100:.1f}%")

# ========== 主菜单 ==========

def print_menu():
    print("\n" + "=" * 32)
    print("   学生信息管理系统 v1.0")
    print("=" * 32)
    print("  1. 添加学生")
    print("  2. 显示全部学生")
    print("  3. 按学号查找")
    print("  4. 修改成绩")
    print("  5. 删除学生")
    print("  6. 数据统计")
    print("  7. 保存数据")
    print("  0. 退出并保存")
    print("=" * 32)

def main():
    students = load_students()
    print(f"已加载 {len(students)} 条学生数据。")
    
    while True:
        print_menu()
        choice = input("请选择:").strip()
        
        if choice == "1":
            add_student(students)
        elif choice == "2":
            show_all(students)
        elif choice == "3":
            search_student(students)
        elif choice == "4":
            modify_score(students)
        elif choice == "5":
            delete_student(students)
        elif choice == "6":
            show_statistics(students)
        elif choice == "7":
            save_students(students)
        elif choice == "0":
            save_students(students)
            print("再见!")
            break
        else:
            print("无效选项,请输入 0~7。")

if __name__ == "__main__":
    main()

这个函数版大约 130 行,但已经是一个完整可用的小系统了。把它保存为 student_system_v1.py,运行,试试添加几个学生、查一下、删一个、保存退出后再启动——数据还在。

版本二:面向对象重构

函数式版本能跑,但当系统变复杂时,所有函数围绕一个全局的 students 列表传来传去的方式会逐渐失控。面向对象版本把数据和操作封装在类里:

import json
from pathlib import Path
from dataclasses import dataclass, asdict

DATA_FILE = Path("students_v2.json")

# ========== 数据模型 ==========

@dataclass
class Student:
    """学生数据类"""
    student_id: int
    name: str
    score: float
    
    def level(self):
        if self.score >= 90: return "A"
        elif self.score >= 80: return "B"
        elif self.score >= 70: return "C"
        elif self.score >= 60: return "D"
        return "F"

# ========== 业务逻辑层 ==========

class StudentManager:
    """学生管理类"""
    
    def __init__(self):
        self.students = []
        self.load()
    
    def _find_index(self, student_id):
        """内部方法:查找学生索引"""
        for i, s in enumerate(self.students):
            if s.student_id == student_id:
                return i
        return -1
    
    def add(self, student):
        if self._find_index(student.student_id) != -1:
            print(f"学号 {student.student_id} 已存在,添加失败。")
            return False
        self.students.append(student)
        print(f"已添加:{student.name}")
        return True
    
    def show_all(self):
        if not self.students:
            print("暂无学生数据。")
            return
        print(f"\n{'学号':<10}{'姓名':<10}{'成绩':<8}{'等级'}")
        print("-" * 36)
        for s in self.students:
            print(f"{s.student_id:<10}{s.name:<10}{s.score:<8.1f}{s.level()}")
    
    def search(self, student_id):
        for s in self.students:
            if s.student_id == student_id:
                return s
        return None
    
    def modify_score(self, student_id, new_score):
        s = self.search(student_id)
        if s is None:
            print("未找到该学号的学生。")
            return False
        s.score = new_score
        print(f"{s.name} 的成绩已更新为 {new_score}")
        return True
    
    def delete(self, student_id):
        idx = self._find_index(student_id)
        if idx == -1:
            print("未找到该学号的学生。")
            return False
        name = self.students.pop(idx).name
        print(f"已删除:{name}")
        return True
    
    def statistics(self):
        if not self.students:
            return None
        scores = [s.score for s in self.students]
        return {
            "总人数": len(self.students),
            "平均分": sum(scores) / len(scores),
            "最高分": max(scores),
            "最低分": min(scores),
            "及格率": sum(1 for s in scores if s >= 60) / len(scores) * 100,
        }
    
    def save(self):
        data = [asdict(s) for s in self.students]
        DATA_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
        print("数据已保存。")
    
    def load(self):
        if not DATA_FILE.exists():
            return
        try:
            data = json.loads(DATA_FILE.read_text(encoding="utf-8"))
            self.students = [Student(**item) for item in data]
        except (OSError, json.JSONDecodeError, TypeError) as e:
            print(f"数据加载失败:{e}")
            self.students = []

# ========== 输入校验 ==========

def read_int(prompt):
    while True:
        try:
            return int(input(prompt))
        except ValueError:
            print("请输入有效的整数。")

def read_float(prompt):
    while True:
        try:
            return float(input(prompt))
        except ValueError:
            print("请输入有效的数字。")

# ========== 主菜单 ==========

def print_menu():
    print("\n" + "=" * 32)
    print("   学生信息管理系统 v2.0")
    print("=" * 32)
    print("  1. 添加学生      2. 显示全部")
    print("  3. 按学号查找    4. 修改成绩")
    print("  5. 删除学生      6. 数据统计")
    print("  7. 保存数据      0. 退出")
    print("=" * 32)

def main():
    manager = StudentManager()
    print(f"已加载 {len(manager.students)} 条学生数据。")
    
    while True:
        print_menu()
        choice = input("请选择:").strip()
        
        if choice == "1":
            sid = read_int("学号:")
            name = input("姓名:").strip()
            if not name:
                print("姓名不能为空。")
                continue
            score = read_float("成绩:")
            if not (0 <= score <= 100):
                print("成绩应在 0~100 之间。")
                continue
            manager.add(Student(sid, name, score))
        
        elif choice == "2":
            manager.show_all()
        
        elif choice == "3":
            sid = read_int("学号:")
            s = manager.search(sid)
            if s:
                print(f"学号:{s.student_id}  姓名:{s.name}  成绩:{s.score:.1f}  等级:{s.level()}")
            else:
                print("未找到。")
        
        elif choice == "4":
            sid = read_int("学号:")
            new_score = read_float("新成绩:")
            if 0 <= new_score <= 100:
                manager.modify_score(sid, new_score)
        
        elif choice == "5":
            sid = read_int("学号:")
            manager.delete(sid)
        
        elif choice == "6":
            stats = manager.statistics()
            if stats:
                print(f"\n===== 统计 =====")
                for key, value in stats.items():
                    if isinstance(value, float):
                        print(f"{key}{value:.1f}")
                    else:
                        print(f"{key}{value}")
        
        elif choice == "7":
            manager.save()
        
        elif choice == "0":
            manager.save()
            print("再见!")
            break
        
        else:
            print("无效选项。")

if __name__ == "__main__":
    main()

在这里插入图片描述

图20-2 数据持久化流程:启动时从 JSON 加载数据到内存 → 运行时所有操作在内存中完成 → 保存/退出时写回 JSON 文件。

三、复盘与展望

为什么这个项目值得你反复回看

学生信息管理系统不是什么炫酷的项目——没有网页界面、没有 AI、没有大数据。但它是一个完美的"技能集成点"——它把你 20 篇学到的几乎所有知识都串联在了一起。每当你学会一个新概念(装饰器、生成器、数据库、日志系统),都可以回到这个项目问自己:“如果在学生管理系统里用这个概念,能怎么改进?”

比如学了装饰器之后,你可以给敏感操作(删除学生)加 @require_confirmation 装饰器。学了生成器之后,可以用生成器来分页显示学生列表(不用一次性构建整个输出)。学了 SQLite 之后,可以把 JSON 存储替换成数据库。学了 logging 之后,可以把 print 换成规范的日志记录。学了 flask 之后,可以把这个终端程序变成 Web 应用。

一个好的练手项目不是"写完就扔"的——它是一个你可以反复迭代的起点。每学一个新技能,就回来改进它,它就会随着你的技能树一同成长。从今天的终端版到明年的全栈 Web 版,这个过程本身就是最好的学习记录。事实上,很多职业程序员的 GitHub 主页上都有这样一个"起点项目"——可能是学生时代写的一个命令行工具,经过多年的迭代变成了一个功能完善的 Web 应用。你不需要一次性写出完美的代码,你只需要持续地回来改进它。这种持续迭代的能力,比任何单次写出的完美代码都更能代表一个开发者的成长潜力。

两个版本的对比

维度函数版 (v1)面向对象版 (v2)
数据存储全局列表 studentsStudentManager.students
操作方式函数 + 传参方法 + self
代码行数~130 行~170 行
扩展难度较难——新增功能需修改多个函数较易——在 Manager 类里加方法即可
适用场景快速原型、小程序中大型项目、团队协作
学号查重手动循环_find_index() 统一封装

函数版适合"快速出活",面向对象版适合"长期维护"。两种写法都是对的——选择取决于项目的规模和生命期。

从函数版到面向对象版——为什么要重构

函数版跑起来没问题——实际上对于 200 行以内的程序,函数式写法可能更直观。但函数版有一个结构性问题:所有操作函数的第一个参数都是 students 这个列表。在你能看到的地方——主菜单里的每次调用——都要显式传递这个列表。这本身没问题,但当程序扩展时——你需要添加一个"按姓名排序"的功能——你现在需要确认每个函数的第一个参数确实是 students,你在新增功能时可能不小心把两个不同的数据结构传给了同一个函数。

面向对象版的核心改进是:students 这个列表不再需要显式传递——它作为 self.students 存储在 StudentManager 的实例里。self 是 Python 替你传递的上下文。添加新功能时,你在 StudentManager 类里加一个新方法,它自动共享 self.students,不需要修改其他任何地方。

从代码量上看,面向对象版反而更多——多了类的定义、多了 self、多了 dataclass。但多出来的代码换来的是结构的清晰和扩展的便捷。这个小项目两种版本都只写了 150 行左右,差别不明显。但当你维护一个上千行的系统时,结构上的差异会被放大十倍。

如果你想让这个项目更进一步

这个系统已经是一个完整的 CRUD(增删改查)应用了。如果你还有余力,可以从以下几个方向继续完善:

功能增强:添加按姓名模糊搜索(输入"张"找到所有姓张的学生)、添加多科成绩支持(语文、数学、英语)、添加成绩排序(按总分/单科)、导出成绩单为 CSV 方便 Excel 打开。

技术升级:用 sqlite3 替换 JSON 文件存储(标准库自带,无需安装)、用 flask 写一个 Web 界面、用 argparse 支持命令行参数(便于脚本化操作)。

代码质量:给类和函数写完整的 docstring、给 StudentManager 写单元测试(unittestpytest)、用 logging 模块替代 print 输出日志。

在这里插入图片描述

图20-3 项目技术点全景:覆盖函数、类、dataclass、列表字典、文件读写、JSON 序列化、异常处理、循环分支等全部核心知识点。

测试你的系统——几种应该检查的场景

写完代码不是结束。花 5 分钟做几个基本的功能测试,确保没有明显的 bug:

场景一:空数据启动。删除 students_v2.json(如果存在),启动程序——应该看到"暂无学生数据",而不是崩溃。

场景二:正常流程。添加 2-3 个学生 → 显示全部 → 搜索一个存在的学号 → 搜索一个不存在的学号 → 修改一个学生的成绩 → 删除一个学生 → 查看统计 → 保存 → 退出。

场景三:数据持久化验证。添加一个学生 → 保存退出 → 重新启动程序 → 显示全部——刚才添加的学生应该还在。

场景四:异常输入。添加学生时输入字母作为成绩 → 删除不存在的学号 → 在成绩栏输入 -10 或 200 → 程序应该给出提示而不崩溃。

如果以上四个场景全部通过,你的系统已经达到了"可演示"的质量水平。如果有场景没通过,回到代码中加对应的校验和异常处理。

从 V2.0 到 V3.0——你可以做得更多

目前的系统用 JSON 文件存储数据——这是最简单的方案。但对于"生产级别"的应用,还有几个自然的发展方向:

用 SQLite 替代 JSON:Python 标准库自带 sqlite3 模块,不需要额外安装。SQLite 把数据存在一个 .db 文件里,支持 SQL 查询——"查找成绩大于 90 的所有学生"用 WHERE score > 90 一句搞定,不需要手写循环和筛选。它还天然支持并发访问和事务——不会出现保存到一半程序崩溃导致数据损坏的问题。

用 Flask 做 Web 界面:把黑底白字的终端菜单换成一个网页界面——在浏览器里操作、有按钮和表单、可以同时让多个用户访问。flask 库轻量且学习曲线平缓,一个晚上就能搭出一个可用的 Web 版学生管理系统。路由设计和函数版系统中的菜单分支几乎一一对应。

用 argparse 支持命令行操作:如果你想在终端里快速地python student.py add --name 小明 --score 85 这样操作,而不是进入交互菜单,argparse 是你需要的标准库。它让你像使用 git commit -m "xxx" 一样使用你自己的程序。

写单元测试:随着功能增多,手动测试变得越来越不可靠。用 unittestpytest 写自动化测试——"添加学生后列表长度应该增加 1"“删除不存在的学号应该返回 False”——每次修改代码后跑一遍测试,确保没有破坏已有的功能。

结语——你已经走了多远

回顾一下 20 篇的历程:

从第一篇不知道 python 命令是什么,到第二十篇能独立写出一个完整的信息管理系统。从 print("Hello World"),到面向对象、装饰器、迭代器、生成器。从复制粘贴代码,到理解每行代码背后的逻辑。

如果你是一路跟着敲过来的,你现在已经具备:

  • 用 Python 解决日常工作中的自动化需求
  • 阅读别人的 Python 代码并理解其结构
  • 独立设计并实现一个小型项目
  • 使用第三方库快速完成复杂任务
  • 看懂 Python 官方文档并自学新模块

更重要的是——你有了"遇到不会的,知道怎么查、去哪查、怎么试"的能力。这才是编程学习中最宝贵的收获。

编程不是看会的,是敲会的。这 20 篇只是一个起点。接下来,选一个你喜欢的方向(Web 开发、数据分析、自动化、AI……),找一个真实的问题,开始写你的项目。

发布你的项目——让代码不只是躺在硬盘里

写完这个系统,你不应该让它只停留在本地文件夹里。几个让这个项目"活起来"的方式:

推送到 GitHub。用 git initgit add .git commit -m "学生信息管理系统 v1.0" 初始化仓库,创建一个 GitHub 仓库,把代码推送上去。这是你的第一个开源项目——不管它有多简单。将来找工作时,你的 GitHub 主页上有真实可运行的代码,比简历上的"熟悉 Python"有说服力得多。

写一个 README.md。至少包含:项目是做什么的、怎么运行(python main.py)、需要什么环境(Python 3.x)、有哪些功能。一个好 README 是你项目的门面——它让偶然点进来的人能在 30 秒内理解这个项目值不值得关注。

加上 requirements.txt。即使这个项目只用到了标准库,也写上 # 本项目仅使用 Python 标准库,无需额外安装依赖。这个小小的文件是告诉别人"我是认真对待这个项目的"的信号。

录一个 30 秒的演示 GIF。运行你的程序,用录屏工具录下添加学生、查看列表、搜索、删除、保存退出的完整流程。把 GIF 放在 README 里。一张动图胜过两百字的功能描述。

这些看起来是"非代码"的工作,但它们是让一个项目从"自己玩"变成"可以展示给世界看"的关键步骤。编程不只是写代码——编程是把想法变成别人能用的产品。而 GitHub + README + 演示就是你的产品发布会。

动手练习

  1. 在面向对象版本的基础上,为 StudentManager 添加"按姓名模糊搜索"方法——输入一个关键字,返回所有姓名中包含该关键字的学生。
  2. 给系统添加"导出 CSV"功能——将当前学生列表导出为 CSV 文件,方便用 Excel 打开。用标准库 csv 模块实现。
  3. 尝试用 sqlite3 标准库替换 JSON 文件存储。数据库表结构自己设计,保留原有的所有功能。对比两种存储方式的代码量和操作感受。
  4. 把这个项目的代码推送到 GitHub,写一个 README.md。这是你 20 篇学习之旅的交付物——一个完整、可运行、有文档的 Python 项目。

要点回顾

  • 函数式版本适合快速理解程序流程,面向对象版本适合长期维护和扩展
  • 数据层(dict/dataclass)、业务层(函数/方法)、持久层(JSON文件)三层架构是小型项目的基本骨架
  • dataclass 让数据模型简洁清晰,StudentManager 封装了所有增删改查逻辑
  • JSON 文件存储是最简单的持久化方案——适合学习和小型项目,数据量大后考虑 SQLite
  • 一个好项目不是"写完就扔"的——它是可以反复迭代、随着技能树一同成长的起点
  • 测试(空数据/正常流程/持久化/异常输入)、README、GitHub 是将项目从"自己玩"升级为"可展示"的关键

下一步学习

学完这 20 篇,你已经具备了用 Python 解决日常问题的核心能力。接下来的学习方向取决于你的兴趣:

  • Web 开发:学 Flask(轻量)或 Django(全栈),把学生管理系统从终端搬到浏览器里
  • 数据分析:深入 pandas、matplotlib、seaborn,用 Jupyter Notebook 做交互式分析
  • 自动化:学 selenium(浏览器自动化)、openpyxl(Excel 自动化)、schedule(定时任务),把工作中重复的手工操作自动化
  • AI/ML:学 numpy、scikit-learn、pytorch,进入机器学习的世界——这也是 Python 最热门的领域
  • 深入 Python:看《流畅的 Python》这本书,理解 Python 的底层机制和高级特性

不管选择哪个方向,记住一个原则:用项目驱动学习。不要只看教程不写代码。每两周给自己一个小目标——做一个天气查询 CLI 工具、写一个 PDF 合并脚本、爬取豆瓣电影 Top 250——项目不需要大,但需要完整(能跑、有文档、能给别人看)。一年之后回头看,你会发现这 20 篇只是起点,而你已经走了很远。

代码是你和计算机之间的对话。现在,你已经学会了这门语言。

更多推荐