Skip to content

章节6 模块综合实战

授课时长:约6-8学时
适用对象:已掌握Python核心编程所有知识点的学员
教学方式:项目驱动 + 分步实现 + 代码审查


一、学习目标

目标分类具体目标
知识目标综合运用数据类型、函数、OOP、文件IO、异常处理、模块化等全部核心知识
技能目标独立完成命令行应用的需求分析、架构设计、编码实现和测试
素养目标培养项目思维:需求→设计→编码→测试→文档的全流程能力

二、项目一:学生信息管理系统(命令行版)

2.1 项目需求分析

2.1.1 功能需求

┌──────────────────────────────────────────────────────────┐
│                  学生信息管理系统                          │
├──────────────────────────────────────────────────────────┤
│  [1] 添加学生    ─ 录入学号、姓名、年龄、性别、班级、成绩    │
│  [2] 查询学生    ─ 按学号/姓名/班级多维度查询               │
│  [3] 修改学生    ─ 修改指定学生的信息                       │
│  [4] 删除学生    ─ 按学号删除学生                           │
│  [5] 显示所有    ─ 以表格形式显示所有学生                   │
│  [6] 统计分析    ─ 各科最高分、最低分、平均分、及格率        │
│  [7] 数据持久化  ─ 数据自动保存到文件,启动时自动加载        │
│  [0] 退出系统    ─ 安全退出                                 │
└──────────────────────────────────────────────────────────┘

2.1.2 核心数据模型

python
"""
学生数据结构(字典存储):
{
    "student_id": "2024001",       # 学号(唯一标识)
    "name": "张三",                 # 姓名
    "age": 20,                      # 年龄
    "gender": "男",                 # 性别
    "class_name": "Python1班",      # 班级
    "scores": {                     # 成绩
        "python": 95,
        "math": 88,
        "english": 92
    }
}
"""

2.1.3 技术栈

技术点用途
字典 / 列表数据存储与操作
面向对象系统架构(类设计)
文件IO + JSON数据持久化
异常处理输入验证与容错
内置函数(sorted/max/min/sum/filter)数据分析统计
f-string格式化输出表格

2.2 系统架构设计

2.2.1 类图设计

┌──────────────────────────────┐
│       StudentManager         │   ← 核心业务逻辑类
├──────────────────────────────┤
│  - students: dict            │   ← 学生数据 {学号: 学生信息}
│  - data_file: str            │   ← 数据文件路径
├──────────────────────────────┤
│  + add_student()             │
│  + find_student()            │
│  + update_student()          │
│  + delete_student()          │
│  + list_all_students()       │
│  + show_statistics()         │
│  + save_to_file()            │
│  + load_from_file()          │
└──────────────────────────────┘

┌──────────────────────────────┐
│         StudentApp           │   ← 用户界面类(CLI)
├──────────────────────────────┤
│  - manager: StudentManager   │
├──────────────────────────────┤
│  + run()                     │   ← 主循环
│  + show_menu()               │
│  + handle_choice()           │
└──────────────────────────────┘

2.2.2 数据流

启动 → 加载数据文件 → 显示菜单 → 用户选择 → 执行操作 → 保存数据 → 返回菜单 → ... → 退出

2.3 分步实现

Step 1: 数据模型与持久化

python
"""student_manager.py — 学生信息管理系统"""

import json
import os
from pathlib import Path
from typing import Optional, List, Dict, Any


class StudentManager:
    """学生信息管理器"""

    def __init__(self, data_file: str = "students.json"):
        self.data_file = data_file
        self.students: Dict[str, dict] = {}  # {student_id: student_info}
        self._load_from_file()

    # ──────────────────────────────────────────
    # 数据持久化
    def _load_from_file(self):
        """从文件加载数据"""
        path = Path(self.data_file)
        if path.exists():
            try:
                with open(path, "r", encoding="utf-8") as f:
                    self.students = json.load(f)
                print(f"✅ 已加载 {len(self.students)} 条学生记录")
            except (json.JSONDecodeError, IOError) as e:
                print(f"⚠️ 数据文件损坏,将使用空数据: {e}")
                self.students = {}
        else:
            self.students = {}

    def _save_to_file(self):
        """保存数据到文件"""
        try:
            with open(self.data_file, "w", encoding="utf-8") as f:
                json.dump(self.students, f, ensure_ascii=False, indent=2)
            return True
        except IOError as e:
            print(f"❌ 保存数据失败: {e}")
            return False

Step 2: CRUD 核心操作

python
    # ──────────────────────────────────────────
    # 添加学生
    def add_student(self, student_info: dict) -> bool:
        """添加学生"""
        student_id = student_info["student_id"]
        if student_id in self.students:
            print(f"❌ 学号 {student_id} 已存在")
            return False

        # 验证成绩
        for course, score in student_info.get("scores", {}).items():
            if not isinstance(score, (int, float)) or score < 0 or score > 100:
                print(f"❌ {course}成绩必须在0-100之间")
                return False

        self.students[student_id] = student_info
        self._save_to_file()
        print(f"✅ 学生 {student_info['name']} 添加成功")
        return True

    # ──────────────────────────────────────────
    # 查询学生
    def find_students(self, keyword: str) -> List[dict]:
        """按关键字查询(支持学号/姓名/班级)"""
        results = []
        keyword = keyword.lower().strip()

        for student in self.students.values():
            if (keyword in student["student_id"].lower()
                    or keyword in student["name"].lower()
                    or keyword in student["class_name"].lower()):
                results.append(student)

        return results

    def get_student(self, student_id: str) -> Optional[dict]:
        """按学号精确查找"""
        return self.students.get(student_id)

    # ──────────────────────────────────────────
    # 修改学生
    def update_student(self, student_id: str, updates: dict) -> bool:
        """修改学生信息"""
        if student_id not in self.students:
            print(f"❌ 学号 {student_id} 不存在")
            return False

        student = self.students[student_id]
        # 不允许修改学号
        updates.pop("student_id", None)

        # 如果修改成绩,需要验证
        if "scores" in updates:
            for course, score in updates["scores"].items():
                if not isinstance(score, (int, float)) or score < 0 or score > 100:
                    print(f"❌ {course}成绩必须在0-100之间")
                    return False

        student.update(updates)
        self._save_to_file()
        print(f"✅ 学生 {student['name']} 信息已更新")
        return True

    # ──────────────────────────────────────────
    # 删除学生
    def delete_student(self, student_id: str) -> bool:
        """删除学生"""
        if student_id not in self.students:
            print(f"❌ 学号 {student_id} 不存在")
            return False

        name = self.students[student_id]["name"]
        del self.students[student_id]
        self._save_to_file()
        print(f"✅ 学生 {name} 已删除")
        return True

    # ──────────────────────────────────────────
    # 获取所有学生
    def get_all_students(self) -> List[dict]:
        """获取所有学生列表"""
        return list(self.students.values())

Step 3: 统计分析功能

python
    # ──────────────────────────────────────────
    # 统计分析
    def show_statistics(self):
        """显示成绩统计信息"""
        if not self.students:
            print("📭 暂无学生数据")
            return

        # 收集所有科目
        all_courses = set()
        for student in self.students.values():
            all_courses.update(student.get("scores", {}).keys())
        all_courses = sorted(all_courses)

        print("\n" + "=" * 80)
        print(" 📊 成绩统计分析报告")
        print("=" * 80)

        total_students = len(self.students)

        for course in all_courses:
            # 提取该科目的所有有效成绩
            scores = []
            for student in self.students.values():
                score = student.get("scores", {}).get(course)
                if score is not None:
                    scores.append(score)

            if not scores:
                continue

            max_score = max(scores)
            min_score = min(scores)
            avg_score = sum(scores) / len(scores)
            pass_count = sum(1 for s in scores if s >= 60)
            pass_rate = pass_count / len(scores) * 100

            print(f"\n  📖 {course}")
            print(f"     参考人数: {len(scores)}")
            print(f"     最高分: {max_score}")
            print(f"     最低分: {min_score}")
            print(f"     平均分: {avg_score:.1f}")
            print(f"     及格人数: {pass_count}")
            print(f"     及格率: {pass_rate:.1f}%")

            # 分数段分布(柱状图)
            levels = {
                "90-100": 0, "80-89": 0, "70-79": 0,
                "60-69": 0, "0-59": 0
            }
            for s in scores:
                if s >= 90: levels["90-100"] += 1
                elif s >= 80: levels["80-89"] += 1
                elif s >= 70: levels["70-79"] += 1
                elif s >= 60: levels["60-69"] += 1
                else: levels["0-59"] += 1

            for level, count in levels.items():
                bar = "█" * count
                print(f"     {level}: {bar} ({count})")

        print("\n" + "=" * 80)

Step 4: 表格展示与输入辅助

python
    # ──────────────────────────────────────────
    # 显示学生表格
    def display_students(self, students: List[dict] = None):
        """以表格形式显示学生信息"""
        if students is None:
            students = self.get_all_students()

        if not students:
            print("📭 暂无学生数据")
            return

        # 表头
        header = f"{'学号':<12} {'姓名':<10} {'年龄':<6} {'性别':<6} {'班级':<15} {'Python':<8} {'数学':<8} {'英语':<8}"
        separator = "─" * len(header)

        print("\n" + separator)
        print(header)
        print(separator)

        for s in students:
            scores = s.get("scores", {})
            row = (
                f"{s['student_id']:<12} "
                f"{s['name']:<10} "
                f"{s['age']:<6} "
                f"{s['gender']:<6} "
                f"{s['class_name']:<15} "
                f"{scores.get('python', '--'):<8} "
                f"{scores.get('math', '--'):<8} "
                f"{scores.get('english', '--'):<8}"
            )
            print(row)

        print(separator)
        print(f"共 {len(students)} 条记录\n")


# ────────────────────────────────────────────────
# 输入辅助函数
def input_int(prompt: str, min_val=None, max_val=None) -> int:
    """输入整数(带验证)"""
    while True:
        try:
            value = int(input(prompt).strip())
            if min_val is not None and value < min_val:
                print(f"❌ 最小值不能小于 {min_val}")
                continue
            if max_val is not None and value > max_val:
                print(f"❌ 最大值不能超过 {max_val}")
                continue
            return value
        except ValueError:
            print("❌ 请输入有效的整数")


def input_float(prompt: str, min_val=None, max_val=None) -> float:
    """输入浮点数(带验证)"""
    while True:
        try:
            value = float(input(prompt).strip())
            if min_val is not None and value < min_val:
                print(f"❌ 最小值不能小于 {min_val}")
                continue
            if max_val is not None and value > max_val:
                print(f"❌ 最大值不能超过 {max_val}")
                continue
            return value
        except ValueError:
            print("❌ 请输入有效的数字")


def input_str(prompt: str, required: bool = True) -> str:
    """输入字符串(带验证)"""
    while True:
        value = input(prompt).strip()
        if not value and required:
            print("❌ 此项不能为空")
            continue
        return value

Step 5: CLI 用户界面

python
class StudentApp:
    """学生信息管理系统命令行界面"""

    def __init__(self):
        self.manager = StudentManager()

    def run(self):
        """主运行循环"""
        print("\n" + "=" * 50)
        print("       🎓 学生信息管理系统")
        print("=" * 50)

        while True:
            self._show_menu()
            choice = input("\n请选择操作 [0-7]: ").strip()

            if choice == "0":
                print("👋 感谢使用,再见!")
                break
            elif choice == "1":
                self._add_student()
            elif choice == "2":
                self._find_student()
            elif choice == "3":
                self._update_student()
            elif choice == "4":
                self._delete_student()
            elif choice == "5":
                self.manager.display_students()
            elif choice == "6":
                self.manager.show_statistics()
            else:
                print("❌ 无效选择,请重新输入")

    def _show_menu(self):
        """显示菜单"""
        print("\n" + "─" * 50)
        print("  [1] ➕ 添加学生")
        print("  [2] 🔍 查询学生")
        print("  [3] ✏️  修改学生")
        print("  [4] 🗑️  删除学生")
        print("  [5] 📋 显示所有学生")
        print("  [6] 📊 成绩统计分析")
        print("  [0] 🚪 退出系统")
        print("─" * 50)

    def _add_student(self):
        """添加学生"""
        print("\n--- ➕ 添加学生 ---")
        student_id = input_str("请输入学号: ")
        name = input_str("请输入姓名: ")
        age = input_int("请输入年龄: ", min_val=1, max_val=150)
        gender = input_str("请输入性别 (男/女): ")
        class_name = input_str("请输入班级: ")

        print("--- 录入成绩 ---")
        scores = {}
        for course in ["python", "math", "english"]:
            score = input_float(
                f"  {course} 成绩 (0-100): ", min_val=0, max_val=100
            )
            scores[course] = score

        student_info = {
            "student_id": student_id,
            "name": name,
            "age": age,
            "gender": gender,
            "class_name": class_name,
            "scores": scores,
        }
        self.manager.add_student(student_info)

    def _find_student(self):
        """查询学生"""
        print("\n--- 🔍 查询学生 ---")
        print("支持按学号、姓名、班级查询")
        keyword = input_str("请输入关键字: ", required=True)

        results = self.manager.find_students(keyword)

        if results:
            print(f"\n找到 {len(results)} 条匹配记录:")
            self.manager.display_students(results)

            # 显示详情
            if len(results) == 1:
                self._show_student_detail(results[0])
        else:
            print("❌ 未找到匹配的记录")

    def _show_student_detail(self, student: dict):
        """显示学生详细信息"""
        print("\n--- 📄 学生详情 ---")
        print(f"  学号: {student['student_id']}")
        print(f"  姓名: {student['name']}")
        print(f"  年龄: {student['age']}")
        print(f"  性别: {student['gender']}")
        print(f"  班级: {student['class_name']}")
        print("  成绩:")
        for course, score in student.get("scores", {}).items():
            print(f"    {course}: {score}")

    def _update_student(self):
        """修改学生信息"""
        print("\n--- ✏️  修改学生 ---")
        student_id = input_str("请输入要修改的学生学号: ")

        student = self.manager.get_student(student_id)
        if not student:
            print(f"❌ 学号 {student_id} 不存在")
            return

        print(f"当前信息: {student['name']} ({student['class_name']})")
        print("(直接回车保持原值)")

        updates = {}

        name = input(f"姓名 [{student['name']}]: ").strip()
        if name:
            updates["name"] = name

        age_str = input(f"年龄 [{student['age']}]: ").strip()
        if age_str:
            updates["age"] = int(age_str)

        gender = input(f"性别 [{student['gender']}]: ").strip()
        if gender:
            updates["gender"] = gender

        class_name = input(f"班级 [{student['class_name']}]: ").strip()
        if class_name:
            updates["class_name"] = class_name

        # 修改成绩
        print("--- 修改成绩(回车保持不变) ---")
        scores = {}
        for course in ["python", "math", "english"]:
            current = student.get("scores", {}).get(course, "")
            score_str = input(f"  {course} [{current}]: ").strip()
            if score_str:
                scores[course] = float(score_str)

        if scores:
            updates["scores"] = {**student.get("scores", {}), **scores}

        if updates:
            self.manager.update_student(student_id, updates)
        else:
            print("没有做任何修改")

    def _delete_student(self):
        """删除学生"""
        print("\n--- 🗑️  删除学生 ---")
        student_id = input_str("请输入要删除的学生学号: ")

        student = self.manager.get_student(student_id)
        if not student:
            print(f"❌ 学号 {student_id} 不存在")
            return

        print(f"即将删除: {student['name']} ({student_id})")
        confirm = input("确认删除? (y/n): ").strip().lower()
        if confirm == "y":
            self.manager.delete_student(student_id)
        else:
            print("已取消删除")

Step 6: 程序入口

python
def main():
    """程序入口"""
    try:
        app = StudentApp()
        app.run()
    except KeyboardInterrupt:
        print("\n\n👋 用户中断,再见!")
    except Exception as e:
        print(f"\n❌ 系统发生未知错误: {e}")
        import traceback
        traceback.print_exc()


if __name__ == "__main__":
    main()

2.4 完整代码

完整代码已在上方分步实现中展示。项目文件结构:

project1_student_manager/
├── main.py                  # 程序入口
├── student_manager.py       # 核心逻辑(StudentManager类)
├── student_app.py           # UI界面(StudentApp类)
├── utils.py                 # 输入辅助函数
├── students.json            # 数据文件(自动生成)
└── README.md                # 项目说明

三、项目二:个人记账本与数据统计工具

3.1 项目需求分析

3.1.1 功能需求

┌──────────────────────────────────────────────────────────┐
│                  个人记账本                                │
├──────────────────────────────────────────────────────────┤
│  [1] 记录收入    ─ 金额、类别、日期、备注                   │
│  [2] 记录支出    ─ 金额、类别、日期、备注                   │
│  [3] 查看账单    ─ 按日期/类别筛选查看                      │
│  [4] 月度统计    ─ 月度收支汇总、分类统计                    │
│  [5] 年度报告    ─ 年度趋势图、各类占比                     │
│  [6] 导出数据    ─ 导出为 CSV 格式                         │
│  [7] 预算管理    ─ 设置月度预算并监控                       │
│  [0] 退出        ─ 退出系统                                │
└──────────────────────────────────────────────────────────┘

3.1.2 数据模型

python
"""
账单记录结构:
{
    "id": "20240315001",          # 唯一ID (YYYYMMDD + 序号)
    "type": "expense",            # 类型: income / expense
    "amount": 100.00,             # 金额
    "category": "餐饮",           # 类别
    "date": "2024-03-15",         # 日期
    "note": "午餐"                # 备注
}
"""

3.1.3 技术栈

技术点用途
面向对象设计系统架构(多类协作)
文件IO + JSON数据持久化
datetime模块日期处理与统计
collections(defaultdict/Counter)数据聚合统计
csv模块数据导出
pathlib路径管理
f-string格式化输出
异常处理输入验证与容错

3.2 系统架构设计

┌──────────────────────────────────────┐
│          FinanceManager              │  ← 财务数据管理器
├──────────────────────────────────────┤
│  - records: list[dict]               │
│  - budget: dict[str, float]          │
│  - data_file: str                    │
├──────────────────────────────────────┤
│  + add_record()                      │
│  + query_records()                   │
│  + monthly_summary()                 │
│  + yearly_report()                   │
│  + export_csv()                      │
│  + set_budget()                      │
│  + check_budget()                    │
└──────────────────────────────────────┘

┌──────────────────────────────────────┐
│           FinanceApp                 │  ← CLI用户界面
├──────────────────────────────────────┤
│  - manager: FinanceManager           │
└──────────────────────────────────────┘

3.3 分步实现

Step 1: 数据模型

python
"""finance_manager.py — 个人记账本"""

import json
import csv
from pathlib import Path
from datetime import datetime, date, timedelta
from collections import defaultdict, Counter
from typing import List, Optional, Dict, Tuple


class FinanceManager:
    """财务管理器"""

    CATEGORIES_INCOME = ["工资", "兼职", "投资", "红包", "其他收入"]
    CATEGORIES_EXPENSE = [
        "餐饮", "交通", "购物", "住房", "娱乐",
        "医疗", "教育", "通讯", "人情", "其他支出"
    ]

    def __init__(self, data_file: str = "finance_data.json"):
        self.data_file = data_file
        self.records: List[dict] = []
        self.budget: Dict[str, float] = {}  # {月份: 预算金额}
        self._load_data()

    def _load_data(self):
        """从文件加载数据"""
        path = Path(self.data_file)
        if path.exists():
            try:
                with open(path, "r", encoding="utf-8") as f:
                    data = json.load(f)
                    self.records = data.get("records", [])
                    self.budget = data.get("budget", {})
                print(f"✅ 已加载 {len(self.records)} 条记账记录")
            except (json.JSONDecodeError, IOError) as e:
                print(f"⚠️ 数据加载失败: {e}")
                self.records = []
                self.budget = {}
        else:
            print("📭 未找到历史数据,将创建新账本")

    def _save_data(self):
        """保存数据到文件"""
        try:
            with open(self.data_file, "w", encoding="utf-8") as f:
                json.dump({
                    "records": self.records,
                    "budget": self.budget
                }, f, ensure_ascii=False, indent=2)
            return True
        except IOError as e:
            print(f"❌ 保存失败: {e}")
            return False

    def _generate_id(self) -> str:
        """生成唯一ID"""
        today = datetime.now().strftime("%Y%m%d")
        # 统计今天已有的记录数
        count = sum(1 for r in self.records if r["id"].startswith(today))
        return f"{today}{count + 1:03d}"

Step 2: 记账核心功能

python
    # ──────────────────────────────────────────
    # 添加记录
    def add_record(self, record_type: str, amount: float,
                   category: str, note: str = "",
                   record_date: str = None) -> bool:
        """添加一条记账记录"""
        if amount <= 0:
            print("❌ 金额必须大于0")
            return False

        if record_type not in ("income", "expense"):
            print("❌ 类型必须是 income 或 expense")
            return False

        if record_date is None:
            record_date = datetime.now().strftime("%Y-%m-%d")

        try:
            # 验证日期格式
            datetime.strptime(record_date, "%Y-%m-%d")
        except ValueError:
            print("❌ 日期格式错误,应为 YYYY-MM-DD")
            return False

        record = {
            "id": self._generate_id(),
            "type": record_type,
            "amount": round(amount, 2),
            "category": category,
            "date": record_date,
            "note": note.strip(),
        }

        self.records.append(record)
        self._save_data()

        type_name = "收入" if record_type == "income" else "支出"
        print(f"✅ {type_name}记录成功: {category} {amount:.2f}元")

        # 检查预算
        if record_type == "expense":
            self._check_budget(record_date)

        return True

    # ──────────────────────────────────────────
    # 查询记录
    def query_records(
        self,
        start_date: str = None,
        end_date: str = None,
        category: str = None,
        record_type: str = None,
    ) -> List[dict]:
        """查询记账记录(支持多维度筛选)"""
        results = self.records.copy()

        if start_date:
            results = [r for r in results if r["date"] >= start_date]
        if end_date:
            results = [r for r in results if r["date"] <= end_date]
        if category:
            results = [r for r in results if r["category"] == category]
        if record_type:
            results = [r for r in results if r["type"] == record_type]

        # 按日期降序排列
        results.sort(key=lambda r: (r["date"], r["id"]), reverse=True)
        return results

Step 3: 统计报表

python
    # ──────────────────────────────────────────
    # 月度统计
    def monthly_summary(self, year: int = None, month: int = None) -> dict:
        """月度收支统计"""
        if year is None:
            year = datetime.now().year
        if month is None:
            month = datetime.now().month

        month_str = f"{year}-{month:02d}"
        month_records = [r for r in self.records if r["date"].startswith(month_str)]

        total_income = sum(r["amount"] for r in month_records if r["type"] == "income")
        total_expense = sum(r["amount"] for r in month_records if r["type"] == "expense")

        # 分类统计
        income_by_category = defaultdict(float)
        expense_by_category = defaultdict(float)
        daily_expense = defaultdict(float)

        for r in month_records:
            if r["type"] == "income":
                income_by_category[r["category"]] += r["amount"]
            else:
                expense_by_category[r["category"]] += r["amount"]
                daily_expense[r["date"]] += r["amount"]

        return {
            "year": year,
            "month": month,
            "total_income": total_income,
            "total_expense": total_expense,
            "balance": total_income - total_expense,
            "income_by_category": dict(income_by_category),
            "expense_by_category": dict(expense_by_category),
            "daily_expense": dict(daily_expense),
            "record_count": len(month_records),
        }

    # ──────────────────────────────────────────
    # 年度报告
    def yearly_report(self, year: int = None) -> dict:
        """年度财务报告"""
        if year is None:
            year = datetime.now().year

        year_str = str(year)
        year_records = [r for r in self.records if r["date"].startswith(year_str)]

        # 每月统计
        monthly_data = {}
        for month in range(1, 13):
            monthly_data[month] = self.monthly_summary(year, month)

        # 年度汇总
        total_income = sum(m["total_income"] for m in monthly_data.values())
        total_expense = sum(m["total_expense"] for m in monthly_data.values())

        # 支出分类占比
        expense_categories = Counter()
        for r in year_records:
            if r["type"] == "expense":
                expense_categories[r["category"]] += r["amount"]

        # 平均每日支出
        days_in_year = 366 if (year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)) else 365
        avg_daily = total_expense / days_in_year

        return {
            "year": year,
            "total_income": total_income,
            "total_expense": total_expense,
            "balance": total_income - total_expense,
            "monthly": monthly_data,
            "expense_by_category": dict(expense_categories),
            "avg_daily_expense": avg_daily,
            "record_count": len(year_records),
        }

Step 4: 预算管理与CSV导出

python
    # ──────────────────────────────────────────
    # 预算管理
    def set_budget(self, month: str, amount: float) -> bool:
        """设置月度预算"""
        if amount < 0:
            print("❌ 预算金额不能为负数")
            return False

        try:
            datetime.strptime(month, "%Y-%m")
        except ValueError:
            print("❌ 月份格式错误,应为 YYYY-MM")
            return False

        self.budget[month] = amount
        self._save_data()
        print(f"✅ {month} 月度预算设置为 {amount:.2f}元")
        return True

    def _check_budget(self, record_date: str) -> Optional[Dict]:
        """检查是否超出预算"""
        month = record_date[:7]  # "YYYY-MM"
        if month not in self.budget:
            return None

        budget_limit = self.budget[month]
        # 计算当月已支出
        month_records = [r for r in self.records
                         if r["date"].startswith(month) and r["type"] == "expense"]
        total_expense = sum(r["amount"] for r in month_records)

        if total_expense > budget_limit:
            print(f"⚠️ 警告!{month} 已超出预算!")
            print(f"  预算: {budget_limit:.2f}元")
            print(f"  已支出: {total_expense:.2f}元")
            print(f"  超出: {total_expense - budget_limit:.2f}元")

        remaining = budget_limit - total_expense
        if remaining < budget_limit * 0.2:
            print(f"💡 提示: 本月预算剩余仅 {remaining:.2f}元")

        return {"budget": budget_limit, "spent": total_expense, "remaining": remaining}

    # ──────────────────────────────────────────
    # CSV导出
    def export_csv(self, filepath: str, records: List[dict] = None):
        """导出记账数据为CSV"""
        if records is None:
            records = self.records

        if not records:
            print("📭 没有可导出的数据")
            return False

        type_map = {"income": "收入", "expense": "支出"}

        try:
            with open(filepath, "w", encoding="utf-8-sig", newline="") as f:
                writer = csv.writer(f)
                # 写表头
                writer.writerow(["ID", "类型", "金额", "类别", "日期", "备注"])
                # 写数据
                for r in records:
                    writer.writerow([
                        r["id"],
                        type_map.get(r["type"], r["type"]),
                        r["amount"],
                        r["category"],
                        r["date"],
                        r["note"],
                    ])
            print(f"✅ 已导出 {len(records)} 条记录到 {filepath}")
            return True
        except IOError as e:
            print(f"❌ 导出失败: {e}")
            return False

Step 5: 界面展示辅助

python
    # ──────────────────────────────────────────
    # 显示记录表格
    def display_records(self, records: List[dict] = None, title: str = "记账记录"):
        """以表格形式显示记账记录"""
        if records is None:
            records = self.records

        if not records:
            print("📭 暂无记录")
            return

        type_symbol = {"income": "📗", "expense": "📕"}
        type_name = {"income": "收入", "expense": "支出"}

        print(f"\n{'=' * 70}")
        print(f"  {title}")
        print(f"{'=' * 70}")
        print(f"{'日期':<12} {'类型':<6} {'类别':<8} {'金额':>10} {'备注':<20}")
        print(f"{'─' * 70}")

        for r in records:
            symbol = type_symbol.get(r["type"], "📘")
            tname = type_name.get(r["type"], r["type"])
            print(f"{r['date']:<12} {symbol+tname:<6} {r['category']:<8} "
                  f"{r['amount']:>10.2f} {r['note']:<20}")

        print(f"{'─' * 70}")

        total_income = sum(r["amount"] for r in records if r["type"] == "income")
        total_expense = sum(r["amount"] for r in records if r["type"] == "expense")
        print(f"  收入合计: {total_income:>8.2f}元")
        print(f"  支出合计: {total_expense:>8.2f}元")
        print(f"  结余:     {(total_income - total_expense):>8.2f}元")
        print(f"{'=' * 70}\n")

    def display_monthly_summary(self, summary: dict):
        """显示月度统计"""
        print(f"\n{'=' * 60}")
        print(f"  📊 {summary['year']}{summary['month']}月 财务报告")
        print(f"{'=' * 60}")
        print(f"  总收入: {summary['total_income']:>10.2f}元")
        print(f"  总支出: {summary['total_expense']:>10.2f}元")
        print(f"  结余:   {(summary['balance']):>10.2f}元")
        print(f"  记录数: {summary['record_count']}条")
        print(f"{'─' * 60}")

        if summary["expense_by_category"]:
            print(f"\n  📤 支出分类:")
            sorted_expense = sorted(
                summary["expense_by_category"].items(),
                key=lambda x: x[1], reverse=True
            )
            for cat, amount in sorted_expense:
                bar = "█" * max(1, int(amount / max(summary["expense_by_category"].values()) * 20))
                print(f"    {cat:<8}: {amount:>8.2f} {bar}")

        if summary["income_by_category"]:
            print(f"\n  📥 收入分类:")
            for cat, amount in summary["income_by_category"].items():
                print(f"    {cat:<8}: {amount:>8.2f}元")

    def display_yearly_report(self, report: dict):
        """显示年度报告"""
        import math

        print(f"\n{'=' * 70}")
        print(f"  📋 {report['year']}年 年度财务报告")
        print(f"{'=' * 70}")
        print(f"  总收入:     {report['total_income']:>10.2f}元")
        print(f"  总支出:     {report['total_expense']:>10.2f}元")
        print(f"  年度结余:   {report['balance']:>10.2f}元")
        print(f"  日均支出:   {report['avg_daily_expense']:>10.2f}元")
        print(f"{'─' * 70}")

        # 月度趋势图
        print(f"\n  📈 月度收支趋势:")
        max_val = max(
            max(m["total_income"], m["total_expense"], 1)
            for m in report["monthly"].values()
        )

        for month in range(1, 13):
            m = report["monthly"][month]
            income_bar = "█" * max(1, int(m["total_income"] / max_val * 20))
            expense_bar = "█" * max(1, int(m["total_expense"] / max_val * 20))
            print(f"  {month:2d}月: 📗{income_bar:<22} 📕{expense_bar:<22}")

        # 支出分类占比
        if report["expense_by_category"]:
            print(f"\n  🥧 支出类别占比:")
            total = sum(report["expense_by_category"].values())
            sorted_cats = sorted(
                report["expense_by_category"].items(),
                key=lambda x: x[1], reverse=True
            )
            for cat, amount in sorted_cats:
                pct = amount / total * 100
                bar = "█" * max(1, int(pct / 2))
                print(f"    {cat:<8}: {amount:>8.2f}元 ({pct:5.1f}%) {bar}")

        print(f"{'=' * 70}\n")

Step 6: CLI 用户界面

python
class FinanceApp:
    """个人记账本命令行界面"""

    def __init__(self):
        self.manager = FinanceManager()

    def run(self):
        """主运行循环"""
        print("\n" + "=" * 50)
        print("       💰 个人记账本")
        print("=" * 50)

        while True:
            self._show_menu()
            choice = input("\n请选择操作 [0-7]: ").strip()

            if choice == "0":
                print("👋 感谢使用,下次记得记账哦!")
                break
            elif choice == "1":
                self._add_income()
            elif choice == "2":
                self._add_expense()
            elif choice == "3":
                self._view_records()
            elif choice == "4":
                self._monthly_report()
            elif choice == "5":
                self._yearly_report()
            elif choice == "6":
                self._export_data()
            elif choice == "7":
                self._budget_management()
            else:
                print("❌ 无效选择,请重新输入")

    def _show_menu(self):
        """显示菜单"""
        print("\n" + "─" * 50)
        print("  [1] 📗 记录收入")
        print("  [2] 📕 记录支出")
        print("  [3] 📋 查看账单")
        print("  [4] 📊 月度统计")
        print("  [5] 📈 年度报告")
        print("  [6] 💾 导出数据")
        print("  [7] 🎯 预算管理")
        print("  [0] 🚪 退出")
        print("─" * 50)

    def _add_income(self):
        """记录收入"""
        print("\n--- 📗 记录收入 ---")
        print(f"  可选类别: {', '.join(FinanceManager.CATEGORIES_INCOME)}")
        category = input("  类别: ").strip()
        amount = float(input("  金额: "))
        note = input("  备注 (可选): ").strip()
        date = input("  日期 (YYYY-MM-DD,回车用今天): ").strip()

        self.manager.add_record("income", amount, category, note,
                                date or None)

    def _add_expense(self):
        """记录支出"""
        print("\n--- 📕 记录支出 ---")
        print(f"  可选类别: {', '.join(FinanceManager.CATEGORIES_EXPENSE)}")
        category = input("  类别: ").strip()
        amount = float(input("  金额: "))
        note = input("  备注 (可选): ").strip()
        date = input("  日期 (YYYY-MM-DD,回车用今天): ").strip()

        self.manager.add_record("expense", amount, category, note,
                                date or None)

    def _view_records(self):
        """查看账单"""
        print("\n--- 📋 查看账单 ---")
        print("  筛选条件(直接回车=全部):")
        start = input("  开始日期 (YYYY-MM-DD): ").strip() or None
        end = input("  结束日期 (YYYY-MM-DD): ").strip() or None
        cat = input("  类别: ").strip() or None
        rtype = input("  类型 (income/expense): ").strip() or None

        records = self.manager.query_records(start, end, cat, rtype)
        self.manager.display_records(records, "查询结果")

    def _monthly_report(self):
        """月度统计"""
        print("\n--- 📊 月度统计 ---")
        year = input("  年份 (回车=今年): ").strip()
        month = input("  月份 (1-12): ").strip()

        year = int(year) if year else None
        month = int(month) if month else None

        summary = self.manager.monthly_summary(year, month)
        self.manager.display_monthly_summary(summary)

    def _yearly_report(self):
        """年度报告"""
        year = input("  年份 (回车=今年): ").strip()
        year = int(year) if year else None

        report = self.manager.yearly_report(year)
        self.manager.display_yearly_report(report)

    def _export_data(self):
        """导出数据"""
        print("\n--- 💾 导出数据 ---")
        filename = input("  文件名 (默认: finance_export.csv): ").strip()
        if not filename:
            filename = "finance_export.csv"
        if not filename.endswith(".csv"):
            filename += ".csv"

        start = input("  开始日期 (回车=全部): ").strip() or None
        end = input("  结束日期 (回车=全部): ").strip() or None

        records = self.manager.query_records(start, end)
        self.manager.export_csv(filename, records)

    def _budget_management(self):
        """预算管理"""
        print("\n--- 🎯 预算管理 ---")
        print("  1. 设置预算")
        print("  2. 查看预算")
        choice = input("  请选择: ").strip()

        if choice == "1":
            month = input("  月份 (YYYY-MM): ").strip()
            amount = float(input("  预算金额: "))
            self.manager.set_budget(month, amount)
        elif choice == "2":
            if self.manager.budget:
                print("\n  当前预算设置:")
                for month, amount in sorted(self.manager.budget.items()):
                    print(f"    {month}: {amount:.2f}元/月")
            else:
                print("📭 尚未设置任何预算")
        else:
            print("❌ 无效选择")


def main():
    """程序入口"""
    try:
        app = FinanceApp()
        app.run()
    except KeyboardInterrupt:
        print("\n\n👋 用户中断,再见!")
    except Exception as e:
        print(f"\n❌ 系统发生未知错误: {e}")
        import traceback
        traceback.print_exc()


if __name__ == "__main__":
    main()

四、项目扩展建议

4.1 学生管理系统扩展

扩展方向实现要点
成绩排名添加按总分/单科排序功能,支持正序和倒序
数据导入支持从Excel/CSV批量导入学生数据
图形界面使用tkinter或PyQt搭建图形界面
数据库存储替换JSON文件存储为SQLite/MySQL数据库
Web版本使用Flask/Django搭建Web版管理系统
批量操作支持批量删除、批量修改班级等操作

4.2 记账本扩展

扩展方向实现要点
图表可视化使用matplotlib生成饼图、柱状图、趋势线
多账本支持创建多个账本(家庭、工作、个人)
定时提醒使用schedule库设置周期性记账提醒
数据加密对敏感财务数据进加密存储
货币换算支持多币种记录和汇率自动换算
消费建议基于历史数据给出智能消费建议

五、项目总结

5.1 知识点覆盖

模块综合实战 知识点清单
├── 基础语法
│   ├── 变量与数据类型(str/int/float/bool)
│   ├── 容器类型(list/dict/tuple/set)
│   ├── 条件判断与循环(if/for/while)
│   └── 推导式与内置函数
├── 函数与模块化
│   ├── 函数定义与参数传递
│   ├── 模块化设计(多文件组织)
│   └── 命名空间与导入机制
├── 面向对象
│   ├── 类与对象设计
│   ├── 封装(属性/方法/私有)
│   ├── 继承与多态
│   └── property/classmethod/staticmethod
├── 高级特性
│   ├── 文件IO(json/csv)
│   ├── 异常处理(try-except)
│   ├── 路径处理(pathlib)
│   └── datetime日期处理
└── 工程实践
    ├── 代码组织与架构分层
    ├── 输入验证与防御性编程
    ├── 数据持久化
    └── 用户交互设计

5.2 复盘要点

  1. 先设计后编码:在写代码之前,先画好类图和数据流图
  2. 分层架构:数据层(Manager)和表现层(App)分离,便于测试和扩展
  3. 防御性编程:对用户输入做充分验证,不给异常留机会
  4. 数据持久化:每次修改数据后立即保存,防止数据丢失
  5. 可读性优先:清晰的命名、适当的注释、统一的代码风格

六、课后练习

基础题

  1. 运行项目:完整运行学生管理系统和个人记账本两个项目,录入不少于10条测试数据。
  2. 功能扩展:为学生管理系统添加"按成绩排名"功能,支持按单科或总分排序。
  3. 数据导出:为记账本添加"按类别汇总导出"功能,导出每个类别的月度汇总。

进阶题

  1. 搜索增强:为学生管理系统添加高级搜索功能,支持多条件组合查询(如:班级+成绩范围+年龄范围)。
  2. 图表可视化:使用 matplotlib 为记账本添加图表功能,生成月度收支趋势折线图和支出类别饼图。
  3. 撤销功能:为学生管理系统添加"撤销上一步操作"功能,至少支持撤销最近3次操作(提示:可以使用命令模式或操作历史栈)。

挑战题

  1. 第三方API集成:为记账本添加汇率查询功能,使用免费汇率API(如 exchangerate-api.com)获取实时汇率,支持多币种记账和自动换算为人民币显示。
  2. Web版改编:使用 Flask 框架将学生管理系统改编为 Web 应用,包含用户登录、数据管理页面、RESTful API 接口和 Bootstrap 前端界面。

参考资料

Python 学习资料