跳转至

周一收尾项目:三个候选规格

三个项目都按同一套"CS50P Final Project 范式"验收:project.py(含 main() 与 ≥3 个可测试的纯函数)+ test_project.py(≥5 个测试)+ README.md。目标代码量 120–200 行。选一个,不要换。规格来源与更成熟的参考实现见 research/06 第三节。

起步骨架在 project/starter/:把 starter/ 重命名project/<你的项目名>/(例如 ledger)再开始。不要复制——两个同名的 test_project.py 会让 pytest 报 "import file mismatch"。骨架里 next_id/load_items/save_items/read_choice 已实现并带测试,作为"纯函数 + 测试"的范例;uv run pytest project -q 一开始就是绿的。


三个候选一览

A. 待办/记账管理器(推荐) B. 终端猜词游戏 C. CSV 成绩分析工具
一句话 命令行菜单增删查改 + JSON 存档 + 统计 Wordle 风格:6 次机会猜 5 字母词,彩色反馈,战绩存档 读班级成绩 CSV,按班级/科目统计、排名、导出汇总
主要知识点 列表里装字典或 dataclass、json、异常、菜单循环、sorted(key=) 字符串算法、setrandom、状态字典、json csv.DictReader/DictWriterstatistics、分组字典、sorted、dataclass
难点 数据一致性(删除后编号)、损坏文件恢复 evaluate_guess 处理重复字母 脏数据行处理、按班级聚合
3 小时风险 中(算法边界) 低(Day 2 Block 7 已预演)
适合谁 想做"真能用"的工具 想做有趣的算法/游戏 想稳妥做完、数据方向

A. 命令行待办 / 记账管理器 ledger

核心功能(MVP,必须全部完成): 1. 菜单循环:1 添加 2 查看 3 删除 4 统计 5 退出,非法输入提示后重试。 2. 添加一条记录:标题、金额(float,可为负表示支出/正表示收入,或只做支出)、分类;自动分配递增 id 与日期(date.today().isoformat())。 3. 查看:按 id 升序打印对齐的表格(用 f-string 宽度)。 4. 删除:按 id 删除,不存在时提示而不崩溃。 5. 统计:总金额、按分类小计(dict 聚合)、金额最大的一条。 6. 持久化:启动时从 data.json 加载,每次修改后保存;文件不存在 → 空列表;JSON 损坏 → 提示并从空列表开始(不覆盖原文件,改名为 data.broken.json)。

建议的纯函数(测试对象)

@dataclass
class Entry:
    id: int
    title: str
    amount: float
    category: str
    date: str

def next_id(entries: list[Entry]) -> int
def add_entry(entries: list[Entry], title: str, amount: float, category: str, today: str) -> Entry   # 返回新条目,追加到 entries
def delete_entry(entries: list[Entry], entry_id: int) -> bool
def total_amount(entries: list[Entry]) -> float
def totals_by_category(entries: list[Entry]) -> dict[str, float]
def format_table(entries: list[Entry]) -> str
def entries_to_dicts(entries: list[Entry]) -> list[dict]     # 给 json.dump 用
def dicts_to_entries(rows: list[dict]) -> list[Entry]
def load_entries(path: Path) -> list[Entry]                  # 处理不存在/损坏
def save_entries(path: Path, entries: list[Entry]) -> None
def main() -> None                                           # 唯一做 input/print 的地方(可再拆 menu 函数)

可选扩展(做 1 个):按月份筛选(date[:7]);预算上限超支提醒;uv add richrich.table 彩色表格;按关键字搜索标题。

测试建议next_id 空列表 → 1;add_entry 后长度 +1 且 id 正确;delete_entry 不存在的 id → False;totals_by_category 两类三条 → 正确聚合;load_entries 对不存在路径 → [],对写入 "{bad" 的临时文件 → []parametrizeformat_table 的对齐或 total_amount 的多组输入;pytest.raises(ValueError)add_entry 空标题。


B. 终端猜词游戏 wordle_cli

核心功能(MVP): 1. 词库:words.txt 每行一个 5 字母英文单词(自备 30–50 个),启动时读入 set[str];文件缺失 → 用内置 10 个词兜底。 2. 每局随机选一个词,6 次机会;输入非 5 字母或不在词库 → 提示且不消耗次数。 3. 每次猜测输出逐位反馈:G(位置正确)、Y(字母存在但位置不对)、X(不存在),并用 ANSI 颜色或方块字符显示。 4. 正确处理重复字母:secret="apple", guess="paper" 的反馈必须按 Wordle 规则(先标 G,再按剩余字母数标 Y)。 5. 局末显示答案与用时次数,询问是否再来一局。 6. 战绩:stats.json 记录总局数、胜场、各次数分布;损坏或缺失 → 从零开始。

建议的纯函数

def load_words(path: Path, fallback: list[str]) -> set[str]
def choose_secret(words: set[str], rng: random.Random) -> str
def is_valid_guess(guess: str, words: set[str]) -> bool
def evaluate_guess(secret: str, guess: str) -> list[str]        # ["G","Y","X","G","X"]
def render_feedback(guess: str, marks: list[str]) -> str
def update_stats(stats: dict, won: bool, attempts: int) -> dict
def load_stats(path: Path) -> dict / def save_stats(path: Path, stats: dict) -> None
def play_round(secret: str, words: set[str], input_func=input) -> tuple[bool, int]   # input_func 便于测试
def main() -> None

可选扩展:显示剩余可用字母表;困难模式(必须沿用已发现的 G 字母);用 rich 上色。

测试建议evaluate_guess("apple", "apple") 全 G;("apple", "paper")["Y","Y","G","Y","X"](自己在纸上推一遍);("abcde", "fghij") 全 X;重复字母 ("aabbb", "bbaaa")is_valid_guess 对长度/词库两种失败;update_stats 胜负各一;load_stats 损坏文件 → 默认;parametrize 覆盖 4 组 evaluate_guess


C. CSV 成绩分析工具 grade_analyzer

核心功能(MVP): 1. 读取 grades.csv(表头 姓名,班级,语文,数学,英语,自备 12–20 行,含 2–3 行脏数据)。 2. 脏数据(空值/非数字/超出 0–100)→ 跳过并记录警告;报表末尾列出警告。 3. 每人平均分与等级(A–F);全体排名(同分按姓名)。 4. 按班级:人数、班级平均、最高分学生。 5. 按科目:平均、中位数、最高、最低(statistics)。 6. ASCII 直方图:[90-100] ****** (6) 五个分段。 7. 导出 summary.csv(姓名、班级、平均分、等级、名次)。

建议的纯函数

@dataclass
class Record: name: str; class_name: str; scores: dict[str, int]
def parse_row(row: dict[str, str], line_no: int) -> Record          # 非法时 raise ValueError(带行号)
def load_records(path: Path) -> tuple[list[Record], list[str]]
def average(record: Record) -> float
def letter_grade(score: float) -> str
def rank(records: list[Record]) -> list[tuple[int, Record, float]]
def class_summary(records: list[Record]) -> dict[str, dict]
def subject_summary(records: list[Record]) -> dict[str, dict[str, float]]
def histogram(averages: list[float]) -> str
def export_summary(path: Path, ranked: list[tuple[int, Record, float]]) -> None
def build_report(...) -> str
def main() -> None

可选扩展:命令行参数 sys.argv[1] 指定 CSV 路径;科目权重 GPA;用 rich 表格。

测试建议parse_row 正常/空值/非数字/超范围(pytest.raises);letter_grade parametrize 五档边界(90、89.9、60、59.9);rank 同分排序;histogram 分段计数;load_records 对临时脏 CSV 返回正确的记录数与警告数。


验收清单(做完逐条勾)

  • uv run python project/<名字>/project.py 能反复运行,第二次运行能看到第一次的数据
  • uv run pytest project -q 全绿,≥5 个测试,含 raisesparametrize
  • uv run ruff check project 零警告,已 ruff format
  • 所有 open/read_text/write_text 都有 encoding="utf-8"
  • 删掉数据文件再运行不崩溃;把数据文件内容改成 {bad 再运行不崩溃
  • 输入 abc、空行、-1 等非法值不崩溃
  • 每个函数 ≤ 25 行,没有全局可变变量
  • README.md 八项齐全
  • AI 评审后改了 ≤3 条并重跑测试通过

3 小时时间线(贴在旁边)

时间 做什么 结束时必须有
0:00–0:50 dataclass/数据约定 → 3 个纯函数 → 3 个测试(先红后绿) pytest 3 passed
0:50–1:00 休息
1:00–1:50 load/save + 损坏恢复 → 菜单循环 → 异常处理 程序能跑完一次"添加→查看→退出→再启动还在"
1:50–2:00 休息
2:00–2:50 补测试到 ≥5 → 1 个扩展 → README → ruff format 验收清单前 8 条全勾

如果 1:50 时菜单还没跑通:第三块全部用于跑通主流程,扩展砍掉,README 写 5 分钟版。