Appearance
章节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 FalseStep 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 valueStep 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 resultsStep 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 FalseStep 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 复盘要点
- 先设计后编码:在写代码之前,先画好类图和数据流图
- 分层架构:数据层(Manager)和表现层(App)分离,便于测试和扩展
- 防御性编程:对用户输入做充分验证,不给异常留机会
- 数据持久化:每次修改数据后立即保存,防止数据丢失
- 可读性优先:清晰的命名、适当的注释、统一的代码风格
六、课后练习
基础题
- 运行项目:完整运行学生管理系统和个人记账本两个项目,录入不少于10条测试数据。
- 功能扩展:为学生管理系统添加"按成绩排名"功能,支持按单科或总分排序。
- 数据导出:为记账本添加"按类别汇总导出"功能,导出每个类别的月度汇总。
进阶题
- 搜索增强:为学生管理系统添加高级搜索功能,支持多条件组合查询(如:班级+成绩范围+年龄范围)。
- 图表可视化:使用 matplotlib 为记账本添加图表功能,生成月度收支趋势折线图和支出类别饼图。
- 撤销功能:为学生管理系统添加"撤销上一步操作"功能,至少支持撤销最近3次操作(提示:可以使用命令模式或操作历史栈)。
挑战题
- 第三方API集成:为记账本添加汇率查询功能,使用免费汇率API(如 exchangerate-api.com)获取实时汇率,支持多币种记账和自动换算为人民币显示。
- Web版改编:使用 Flask 框架将学生管理系统改编为 Web 应用,包含用户登录、数据管理页面、RESTful API 接口和 Bootstrap 前端界面。
参考资料
- Python JSON 文档:https://docs.python.org/zh-cn/3/library/json.html
- Python CSV 文档:https://docs.python.org/zh-cn/3/library/csv.html
- Python datetime 文档:https://docs.python.org/zh-cn/3/library/datetime.html
- Python collections 文档:https://docs.python.org/zh-cn/3/library/collections.html
- Flask 官方教程:https://flask.palletsprojects.com/
- matplotlib 教程:https://matplotlib.org/stable/tutorials/