综合练习与下一步
小脚本、数据分析与 AI 路线指引。
综合练习与下一步
这一课你将能做到
- 独立完成一个多文件小项目「记账本」:数据类 + 文件持久化 + 异常兜底 + 交互循环
- 用十二课学到的每一个模块对应项目里的具体代码位置,形成「知识地图」
- 说清从 Python 基础出发的三条进阶路线(数据 / AI / 工程化)各自要补什么
为什么要学
零散知识不等于能力。前十一课你学了缩进、类型、循环、函数、容器、字符串、文件、模块、类、异常——但只有把它们装进同一个真实程序里,才能检验哪些真会了、哪些只是「看过」。这一课的项目刻意覆盖全部十二课的考点,做完它,Python 基础就算真正闭环;做完之后去哪,这一课也给你地图。
人话:前面是学招式,这一课是打一场完整的实战。招式背得再熟,实战里忘了出招顺序(先存数据还是先算统计)才算真掌握。
先建立图像
记账本(expense tracker)项目结构:
python-lab/
├── .venv/ ← 第 9 课:虚拟环境
├── models.py ← 第 10 课:数据类
├── storage.py ← 第 8 课:JSON 文件持久化
├── stats.py ← 第 6/7 课:容器统计与字符串格式化
├── main.py ← 第 2/4/11 课:交互循环 + 异常兜底
└── expenses.json ← 数据文件(运行后自动生成)
程序数据流:
用户输入 → 解析校验(异常) → 记账对象(类) → 存盘(文件/JSON)
↘ 统计报表(字典统计 + f-string)
跟着做
步骤 1:搭好项目骨架(第 9 课)
cd python-lab
python -m venv .venv
.venv\Scripts\activate # macOS/Linux: source .venv/bin/activate
新建空文件 models.py、storage.py、stats.py、main.py。本步验收:终端提示符前有 (.venv),四个文件就位。
步骤 2:写数据层 models.py(第 10 课:类)
"""记账记录的数据模型"""
from datetime import datetime
class Expense:
def __init__(self, category: str, amount: float, note: str = ""):
if amount <= 0:
raise ValueError(f"金额必须为正数,收到 {amount}") # 第 11 课
self.category = category
self.amount = amount
self.note = note
self.created_at = datetime.now().strftime("%Y-%m-%d %H:%M")
def to_dict(self) -> dict:
return {
"category": self.category,
"amount": self.amount,
"note": self.note,
"created_at": self.created_at,
}
@classmethod
def from_dict(cls, data: dict) -> "Expense":
"""从字典重建对象(读文件时用)"""
expense = cls(data["category"], data["amount"], data.get("note", ""))
expense.created_at = data["created_at"]
return expense
def __str__(self) -> str:
return f"{self.created_at} {self.category:<6} {self.amount:>8.2f} 元 {self.note}"
要点回顾:__init__ 校验非法数据直接 raise;to_dict / 负责「对象 ↔ 字典」互转(JSON 只认字典); 让 print 出人话; 是不依赖具体实例的构造入口——这是你见到的第一个装饰器,会用即可。
from_dict
__str__
@classmethod
步骤 3:写持久化层 storage.py(第 8 课:文件 + JSON)
"""JSON 文件持久化"""
import json
from pathlib import Path
from models import Expense
DB_PATH = Path(__file__).parent / "expenses.json" # 永远定位到脚本目录
def load_all() -> list[Expense]:
if not DB_PATH.exists():
return []
try:
with open(DB_PATH, encoding="utf-8") as f:
raw = json.load(f)
return [Expense.from_dict(item) for item in raw] # 第 6 课:推导式
except json.JSONDecodeError:
print("警告:数据文件损坏,已按空账本启动")
return []
def save_all(expenses: list[Expense]) -> None:
with open(DB_PATH, "w", encoding="utf-8") as f:
json.dump([e.to_dict() for e in expenses], f, ensure_ascii=False, indent=2)
要点回顾:with open + 显式 encoding="utf-8";ensure_ascii=False 保中文;文件损坏不崩程序而是降级处理;list[Expense] 这种类型注解 3.9+ 可用,属可读性增强。
步骤 4:写统计层 stats.py(第 6/7 课:容器与字符串)
"""统计报表"""
from collections import defaultdict
from models import Expense
def total(expenses: list[Expense]) -> float:
return sum(e.amount for e in expenses) # 生成器表达式
def by_category(expenses: list[Expense]) -> dict[str, float]:
result: dict[str, float] = defaultdict(float)
for e in expenses:
result[e.category] += e.amount
return dict(result)
def report(expenses: list[Expense]) -> str:
if not expenses:
return "账本是空的,先记一笔吧"
lines = [f"共 {len(expenses)} 笔,合计 {total(expenses):.2f} 元", "-" * 36]
for cat, amount in sorted(by_category(expenses).items(), key=lambda kv: -kv[1]):
percent = amount / total(expenses)
bar = "█" * round(percent * 20) # 文本进度条
lines.append(f"{cat:<8}{amount:>9.2f} 元 {percent:6.1%} {bar}")
return "\n".join(lines) # 第 7 课:join
共 3 笔,合计 58.50 元
------------------------------------
餐饮 45.50 元 77.8% ███████████████
交通 8.00 元 13.7% ██
娱乐 5.00 元 8.5% █
要点回顾:defaultdict 免去 get 初始化;sorted 的 key 用 lambda 按金额倒序;f-string 的对齐、百分比格式一次用全。
步骤 5:写主程序 main.py(第 2/4/11 课:循环 + 异常)
"""记账本主程序:交互循环"""
from models import Expense
from storage import load_all, save_all
from stats import report, by_category
MENU = """--- 记账本 ---
1. 记一笔
2. 看全部
3. 分类统计
4. 退出"""
def read_positive_float(prompt: str) -> float:
"""带重试的数字输入(第 11 课的落地)"""
while True:
raw = input(prompt).strip()
try:
value = float(raw)
if value <= 0:
print("金额要大于 0")
continue
return value
except ValueError:
print(f"“{raw}”不是数字,请重输")
def main():
expenses = load_all()
while True: # while True + break 惯用法
print(MENU)
choice = input("选择操作:").strip()
if choice == "1":
category = input("分类(餐饮/交通/娱乐…):").strip() or "其他"
amount = read_positive_float("金额:")
note = input("备注(可回车跳过):").strip()
expenses.append(Expense(category, amount, note))
save_all(expenses) # 每记一笔立即落盘
print("已记账 ✓")
elif choice == "2":
for i, e in enumerate(expenses, start=1): # 第 4 课:enumerate
print(f"{i}. {e}")
elif choice == "3":
print(report(expenses))
elif choice == "4":
print("再见,记得看报表")
break
else:
print("没有这个选项,重选")
if __name__ == "__main__": # 第 9 课:双模式开关
main()
运行 python main.py,按菜单走一遍:记两三笔 → 看全部 → 看统计 → 退出。重新运行程序,数据还在(storage 落盘生效)。
--- 记账本 ---
1. 记一笔
2. 看全部
3. 分类统计
4. 退出
选择操作:1
分类(餐饮/交通/娱乐…):餐饮
金额:abc
“abc”不是数字,请重输
金额:45.5
备注(可回车跳过):午饭
已记账 ✓
步骤 6:给自己出三道升级题(检验闭环)
- 加「删除某笔」功能:main 里加选项 5,按序号
expenses.pop(i - 1),注意序号越界要 try/except 或先判断。
- 按月份过滤报表:给 stats.report 加参数
month="2026-10",用 e.created_at.startswith(month) 过滤(第 7 课 startswith)。
- 支出上限告警:类属性
MONTHLY_LIMIT = 2000,统计时若 total(expenses) > MONTHLY_LIMIT 打印红色告警文本——想想该放类属性还是实例属性(第 10 课)。
每道题都对应明确课程:改完这三题,类型、容器、字符串、文件、类、异常、循环全部二次落地。
讲清楚要点
- 分层是项目感的起点:models(是什么)→ storage(存哪里)→ stats(算什么)→ main(怎么交互)。每层只管一件事,改统计不用碰存储——这就是第 5 课「函数只做一件事」在项目尺度的重演。
- 数据在层间「变形」:界面上是字符串 → 校验后是 float → 业务里是 Expense 对象 → 存盘时是 dict/JSON → 读回来再变回对象。每一跳用什么函数(float()、to_dict、json.dump)要能脱口而出。
- 「每记一笔就落盘」是简单可靠的策略:练习阶段不值得引入数据库,JSON 文件 + 每次全量重写足够;能说清它在大数据量下的局限(全量重写慢、无并发锁)就具备了谈「何时该换 SQLite/数据库」的判断力。
- 异常策略在项目里的分工:模型层 raise(拦截非法数据)、输入层 try/except + 重试(兜住用户手滑)、存储层捕获降级(文件坏了不崩程序)。三层三种姿态,正好是第 11 课「何时捕获、何时让它崩」的答案。
- 对象 ↔ 字典互转是 Python 工程的高频动作:API 响应、数据库行、JSON 文件全是字典,业务代码里是对象。写熟
to_dict/from_dict,将来见到 dataclass、pydantic 就秒懂——它们只是把这个样板代码自动化了。
你可能会懵的地方
-
这个项目为什么不用数据库?
基础收官项目的目标是「巩固语言本身」,文件 + JSON 恰好覆盖第 8 课全部考点且零依赖。数据库(SQLite 一行 import 就能用)是下一步进阶的练习对象,放进路线图而不是塞进这一课。
-
from_dict 里 data["amount"] 不怕 KeyError 吗?
数据文件是本程序自己写入的,字段必然存在——内部约定可以信任(信任内部数据是基本工程判断)。若是外部来源(用户上传、第三方接口),就该逐字段校验或捕获 KeyError。区分「内部可信数据」和「外部不可信输入」是异常策略的核心。
-
@classmethod 和普通方法差在哪?什么时候用?
普通方法第一个参数是实例(self);类方法第一个参数是类(cls),不依赖任何实例。Expense.from_dict(...) 在还没有对象时就要造对象,只能类方法。见过的用法还有 dict.fromkeys()。
-
想给它加 Web 界面,该学什么?
先学 HTTP 基础 + requests 调 API(把 CLI 的统计结果用 JSON 吐出去),再选 Flask/FastAPI 起 Web 服务。那是 python 路线图下一站的内容,本项目正好可以当数据源改造。
-
代码里 list[Expense]、dict[str, float] 这些是什么?
类型注解:3.9+ 内置容器支持直接下标写法,运行时不强制,但 IDE 补全、mypy 静态检查都靠它。基础阶段建议养成「函数签名写注解」的习惯,函数体内不必过度标注。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 所有代码塞一个 main.py | 三百行后没法维护 | 按职责拆模块(models/storage/stats/main) |
| 统计时重复调 total() | 每次全量重算,浪费且易不一致 | 先算一次存变量再复用 |
| 退出前才统一存盘 | 程序崩溃丢全部数据 | 关键操作后立即落盘 |
| 输入金额不校验直接 float() | 用户手滑整个程序崩 | try/except + 重试循环 |
删除按 pop(i) 不处理越界 | IndexError 崩溃 | 先判断 0 <= i < len 或捕获异常 |
| 报表函数里 print 而不是 return | 结果没法被测试和复用 | 返回字符串,print 留给 main |
卡点急救
- ModuleNotFoundError: models:没在项目目录运行,或 venv 没激活。
cd python-lab 后再 python main.py。
- expenses.json 里中文是 \uXXXX:json.dump 漏了
ensure_ascii=False,补上重存。
- 报表金额加总对不上:检查是否混入了字符串金额(读回 JSON 后没转 float),在 from_dict 里显式
float(data["amount"])。
- while 循环退不出去:choice 分支里的
break 缩进错进了 else。确认 break 与 if/elif 同层。
- 改了 stats.py 不生效:Python 模块缓存——确认保存了文件、运行的是当前解释器(venv 激活状态);IDE 里可能跑的是旧缓存,重开终端验证。
面试常问
-
给你一个需求(如记账/待办清单),你如何设计程序结构?
答题框架:分层(模型/存储/逻辑/入口)→ 数据流(输入校验 → 业务对象 → 持久化 → 统计输出)→ 异常策略(哪里 raise、哪里 catch)→ 可扩展点(换存储、加导出)。能对上本课项目每一层的设计理由即可。
-
dict 和对象各适合承载业务数据吗?怎么选?
dict 适合临时、动态、跨边界传输(JSON、API);对象适合字段固定、带行为和校验的业务实体。工程上常见「边界用 dict、核心用对象」,用 to_dict/from_dict 显式转换,或用 dataclass/pydantic 减少样板。
-
如何保证用户输入不会让程序崩溃?
三层防线:输入层 try/except 转换失败并重试;校验层 raise 值错误并给出可读信息;主循环兜底捕获未知异常并记录日志。核心是「外部输入永远不可信」。
-
用 JSON 文件做持久化有什么局限?什么时候升级方案?
局限:全量读写、无并发控制、无索引查询、数据量大内存压力大。升级信号:数据量上万条、多进程/多用户写、需要按条件查询——此时换 SQLite(标准库 sqlite3,零部署)。
-
这个基础课程之后,Python 学习路线怎么走?
按目标分流:数据处理 → pandas/numpy;Web/API → requests、FastAPI;AI 应用 → 大模型 SDK、prompt 工程;工程质量 → 类型注解 + mypy、pytest、logging。共同前提是先把标准库用熟(本课 1-11 节)。
自测三问
- 用户输入
45.5 到它在 JSON 文件里被存下,中间经历了哪几次「形态转换」?各用什么函数?
- models / storage / stats / main 四个模块各自的单一职责是什么?为什么统计函数返回字符串而不是直接 print?
- 升级题 2(按月过滤)你打算改哪几个文件的哪几行?用到了哪些课的知识点?
小结一句话
十二课的语法点在「记账本」里各就各位:类管数据、文件管持久化、容器管统计、异常管输入、循环管交互——能独立改出三个升级功能,Python 基础就真正闭环了。
延伸