第30章-项目完善、答辩与阶段测验


这一周的代码到今天已经攒了五版,今天是把它们收拢成"最终版"的日子。

明天就验收了,今天干三件事:精装修、写讲稿、彩排

精装修,是给项目加加分项——修改、校验、排序、统计,这些不是必须的,但做了就是亮点,答辩时老师问"有什么亮点",你能拿出真东西。写讲稿,是把项目"讲"明白——代码写得再好,讲不清楚等于白做,因为老师要确认"这是你理解的,不是 AI 代考的"。彩排,是提前把演示流程走一遍,明天上台不慌张。今天结束时,你的电脑里是一个能演示、能讲、有 Git 提交记录的完整作品——这是你 6 周以来的第一个"作品",值得认真对待。

周六就是上机测验 + 项目答辩,本章最后附上了周测的详细说明,对照它做最后冲刺。

进阶功能一:修改联系人

修改 = 按编号找到人 → 重新输入电话 → 存回原位置。它综合了这几天学过的所有零件:编号换算(num - 1)、边界检查、try/except、保存文件。

修改和删除的思路几乎一样,唯一区别是:删除用 pop 把元素拿走,修改是"原地改"——用下标把新值放回原位置。

def edit_contact(contacts):
    show_contacts(contacts)              # 先让用户看到编号
    if not contacts:
        return
    try:
        num = int(input("输入要修改的编号:"))
    except ValueError:
        print("请输入数字!")
        return
    if 1 <= num <= len(contacts):
        new_phone = input("输入新的电话:")
        contacts[num - 1]["phone"] = new_phone   # 原地修改字典的 phone 字段
        save_contacts(contacts)          # 改完保存
        print("修改成功!")
    else:
        print("编号无效")

关键一行 contacts[num - 1]["phone"] = new_phone 是"两步合一":先按下标 num - 1 取到那个联系人的字典,再往字典的 "phone" 键里放新值。就像先翻到第 N 页,再改那一页上的电话号码。

挂到菜单上:菜单扩成 6 项 1.添加 2.查看 3.搜索 4.删除 5.修改 6.退出,加 elif choice == "5": edit_contact(contacts),退出变成 6。

三个容易漏的点。字段不止能改电话,contacts[num-1]["name"] = input("新的姓名:") 也行,但只改用户输入的那个字段,别的字段别动,否则会覆盖掉原数据。改完必须 save_contacts(contacts)——"凡数据变化必保存"这条铁律别忘了。编号换算、try/except、边界检查这三件套和删除一模一样,抄一遍加深记忆。

进阶功能二:数据校验

数据校验 = 在数据入库前检查"合不合法",不合法就要求重输。就像银行柜台:证件不合格就不办业务,直接告诉你缺什么。电话的校验规则:必须全是数字,且长度 11 位(中国手机号)。

def add_contact(contacts):
    name = input("姓名:")
    while True:                                   # 循环:不合法就重新输入
        phone = input("电话(11位数字):")
        if phone.isdigit() and len(phone) == 11:  # 校验:全是数字 且 长度11
            break                                 # 合法,跳出循环
        print("电话必须是 11 位数字,请重新输入!")
    contacts.append({"name": name, "phone": phone})
    save_contacts(contacts)
    print("添加成功!")

校验两件套:phone.isdigit() 判断是不是全数字——只要有一个字母就返回 False;len(phone) == 11 判断长度够不够——不等于 11 就返回 False。两个条件用 and 连起来,同时满足才合法,缺一个都不行。

while True + break 是校验的标准套路:一直循环让用户输入,合法才 break 放行,不合法就提示重来。门卫一直守在门口,证件合格才放人进去。

为什么要校验?脏数据(不是 11 位的电话)会污染你的数据,以后搜索、统计都会出错。数据质量是程序的良心,校验是数据质量的守门员。两个细节:isdigit() 对空字符串返回 False(空字符串没有数字),所以不需要额外判断空;校验只做"输入格式"检查,不需要 try/except——isdigit() 是"先检查再转换",本身就是防错手段,和 int() 转换的错误类型不同。

想校验备注的话,规则就得自己定:备注允许为空(不是必填),所以只能限制长度,比如不超过 20 个字。校验规则本来就是需求的一部分,"校验什么、怎么校验"由你自己定,这正好是答辩能讲的一个点。

进阶功能三:排序与统计

排序 = 让联系人按规则排好队统计 = 数一数有多少个。排序用 sorted() + key=lambda(这一周的核心难点,看懂会用即可),统计用 len()

# 统计:共有多少联系人(最简单)
def count_contacts(contacts):
    print(f"通讯录共有 {len(contacts)} 个联系人")

# 排序:按姓名拼音/字母顺序显示(最花时间,也最出彩)
def show_sorted(contacts):
    sorted_contacts = sorted(contacts, key=lambda c: c["name"])
    for i, c in enumerate(sorted_contacts, 1):
        print(f"{i}. {c['name']} - {c['phone']}")

sorted 拆开看:sorted(contacts, ...) 把 contacts 按某种规则排好序,生成一个新列表(原列表不动);key=lambda c: c["name"] 是"取什么作为排序依据"——按每个字典的 name 字段排;lambda c: c["name"] 是匿名小函数,一个用完就扔的临时函数:给一个联系人 c,返回它的姓名。

为什么不能直接 sorted(contacts)?列表里是字典,Python 不知道"按什么排"。你必须告诉它:按 name 排(key=lambda c: c["name"]),或者按电话排(key=lambda c: c["phone"])。就像排队买票,得先规定"按到达顺序"还是"按年龄"排。

记账本版的统计,是"分类累加"的思路:

def stats(records):
    total_income = 0
    total_expense = 0
    for r in records:
        if r["type"] == "收入":
            total_income += r["amount"]
        else:
            total_expense += r["amount"]
    print(f"总收入:{total_income} 元")
    print(f"总支出:{total_expense} 元")
    print(f"结余:{total_income - total_expense} 元")

遍历每笔账,看 type 是收入还是支出,分别累加到两个变量里——"遍历 + 判断 + 累加"的经典组合,和搜索的思路一脉相承。

三个小提醒。sorted 返回新列表,不改原列表,想改原列表要用 contacts.sort(...),这里用 sorted 就行。lambda c: c["name"] 的键名要和数据结构统一,写错键名照样 KeyError。中文字符排序默认按 Unicode 码点排(大致是拼音顺序的近似),不用深究,能排出稳定顺序即可。金额累加注意 += 写法:total_income += r["amount"] 就是 total_income = total_income + r["amount"]

完整版参考:对照查漏补缺

下面是这几天所有代码的"最终合体"。正确用法:先用自己的代码对照逐行看,缺什么补什么(尤其是注释和异常处理),而不是直接复制覆盖自己的。代码是你的,你得认得每一行。

import json

FILE = "contacts.json"

def load_contacts():
    try:
        with open(FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        return []

def save_contacts(contacts):
    with open(FILE, "w", encoding="utf-8") as f:
        json.dump(contacts, f, ensure_ascii=False, indent=2)

def add_contact(contacts):
    name = input("姓名:")
    phone = input("电话:")
    contacts.append({"name": name, "phone": phone})
    save_contacts(contacts)
    print("添加成功!")

def show_contacts(contacts):
    if not contacts:
        print("通讯录是空的")
        return
    for i, c in enumerate(contacts, 1):
        print(f"{i}. {c['name']} - {c['phone']}")

def search_contact(contacts):
    kw = input("输入要搜索的姓名或电话:")
    found = False
    for c in contacts:
        if kw in c["name"] or kw in c["phone"]:
            print(f"{c['name']} - {c['phone']}")
            found = True
    if not found:
        print("没找到")

def delete_contact(contacts):
    show_contacts(contacts)
    if not contacts:
        return
    try:
        num = int(input("输入要删除的编号:"))
    except ValueError:
        print("请输入数字!")
        return
    if 1 <= num <= len(contacts):
        removed = contacts.pop(num - 1)
        save_contacts(contacts)
        print(f"已删除 {removed['name']}")
    else:
        print("编号无效")

def main():
    contacts = load_contacts()
    while True:
        print("\n1.添加  2.查看  3.搜索  4.删除  5.退出")
        choice = input("请选择:")
        if choice == "1":
            add_contact(contacts)
        elif choice == "2":
            show_contacts(contacts)
        elif choice == "3":
            search_contact(contacts)
        elif choice == "4":
            delete_contact(contacts)
        elif choice == "5":
            break
        else:
            print("输入无效,重新选择")

main()

对照自查,一条条过:

检查项 自己代码里有吗
每个函数都有 docstring("""...""" 注释说明功能)
load_contacts try 住了 FileNotFoundError
所有 int(input(...)) 都有 try/except
所有增删改后都调用了 save_contacts
文件名常量 FILE 统一(写错路径文件就读不到了)
功能函数都带 contacts 参数,没有直接访问全局变量

对照时逐函数看,别一眼扫过去"感觉差不多"就跳过——漏一个 try,周测就可能当场崩溃。风格上注意:main() 放最后再调用;函数之间空两行;变量名用"名词 + 下划线"(total_income),别用 ab 这种。每改一处就运行一次测试,别攒到最后一起跑——bug 堆在一起就分不清是谁的锅了。

这份参考代码本身就是你的"答辩答案":每个函数是干嘛的、参数传什么、为什么这么写,对照自查一遍,等于把答辩讲稿也顺了一遍。

答辩准备:三个问题讲清项目

答辩 = 向老师证明"这个项目真是我做的、我懂的"。不用背稿,抓住三个问题就够——这也是以后所有项目答辩的通用套路

问题 怎么理解 怎么准备
① 做了什么 项目有哪些功能 演示一遍:添加 → 查看 → 搜索 → 删除 → 重启数据还在
② 怎么做的 数据结构和实现思路 讲数据结构(列表套字典)、每个函数怎么实现
③ AI 帮了什么 人机分工 哪些是 AI 写的、哪些是你改的、遇到 bug 怎么问的

对着三个问题自己过一遍(讲给同桌听):

问题 1:做了什么?
回答框架:这是一个通讯录程序,能添加、查看、搜索、删除联系人,数据存在
contacts.json 文件里,关掉程序再打开数据还在。我演示给你看……

问题 2:怎么做的?
回答框架:一个联系人用一个字典 {name, phone},所有联系人在一个列表里,
每个功能一个函数。添加就是往列表 append 一个字典;搜索就是遍历列表,
用 in 判断关键字;删除就是用户输编号,编号减 1 当下标用 pop 删;
每次改动数据后调用 save_contacts 把列表存成 JSON 文件。

问题 3:AI 帮了什么?
回答框架:骨架是我照着例子敲的;遇到 ValueError 的报错,我用三步法问 AI,
AI 说 int() 转不了"abc",建议用 try/except,我照着改的;
代码检查时 AI 帮我补了注释、发现了漏掉的 save_contacts。

老师还常在一分钟里随机提问,答案提前想好:"这个函数是干嘛的?"(随便指一个函数,说功能 + 参数 + 返回值);"为什么这里用 try?"(答:int() 可能转不了用户输入,报 ValueError,try 接住防止程序崩溃);"为什么删除用 num - 1?"(答:显示编号从 1 开始,下标从 0 开始,编号 = 下标 + 1)。

有一句老话放在这儿:讲不清 = 没掌握。答辩能讲明白,说明这 6 周没白学。练习时别背稿——背出来的和理解的,一听就分得出来,用"讲给同桌听"的方式练,讲着讲着就顺了。

讲着讲着卡住了怎么办?这正是彩排的价值——卡住的地方就是你没吃透的地方,回代码里把它弄明白,再讲一遍。等你能不看代码、一口气讲完三个问题,明天的答辩就稳了。

演示用的数据提前准备好:2~3 个有辨识度的联系人(比如"妈妈 138..."、"老同学 139..."),演示时一眼能看出添加、搜索的效果。"故意输错"的演示也提前设计好:删除时输入"abc"、输入超范围编号 9——正好展示你的异常处理和提示友好,这是加分镜头。

Git 提交:给作品存档

Git 提交 = 给代码拍一张"存档照",随时能回到这个版本。这是第 1 周学过的技能,今天正式用在自己的第一个作品上。在项目文件夹里打开终端(Git Bash),依次执行:

# 第一次:初始化仓库(如果这周还没建过)
git init

# 把文件加入"待提交区"(add 是"选中"要提交的文件)
git add contacts.py contacts.json

# 提交(commit 是"正式存档",-m 后面写存档说明)
git commit -m "完成通讯录项目:添加/查看/搜索/删除 + JSON持久化"

两个命令的分工,口诀先 add 再 commit:git add 文件名 是告诉 Git"这几个文件我要存档"(选中);git commit -m "说明" 是正式拍照存档,说明写"这次改了什么"。

提交说明怎么写才专业?一句话讲清"这次干了什么",用动词开头:完成添加和查看功能加入JSON持久化修复删除时的编号越界bug。别写 更新修改 这种没说清的话。如果之前已经 add 过一次、又改了代码:直接再次 git add contacts.py + git commit 就行,新的存档会覆盖旧的快照历史(旧版本通过 git log 还能看到)。

几个要点:提交前先确认代码跑通——存档一个"跑不通的半成品",回滚时反而添乱,测试通过再提交是职业习惯。git add 后面跟的是文件路径,漏了路径或写错文件名,Git 会提示找不到文件。git commit 忘记 -m "..." 会进入一个编辑器(Vim),新手容易卡住,养成习惯每次都带 -m。提交前可以 git status 看一眼:红色 = 还没 add,绿色 = 已 add 待提交,确认无误再 commit。

这周的建议节奏是每天提交一次:周一建项目提交一次,之后每天收工时提交一次。这样任何一天改坏了,都能回到昨天的版本重来,心里有底。

演示彩排:明天上台不慌

彩排 = 按明天的流程完整走一遍,发现并提前解决"演示时会翻车"的问题。演示最怕三件事:程序崩了、数据是空的、紧张忘词。彩排就是给这三件事打预防针。

明天答辩就按这个流程走:

步骤 动作 检查点
1 启动程序 菜单正常显示
2 添加 1 个联系人 提示"添加成功"
3 查看全部 带编号、中文不乱码
4 搜索"故意输对"的关键字 能搜出结果
5 搜索一个不存在的关键字 提示"没找到"(演示友好提示)
6 删除时输入"abc" 提示"请输入数字"不崩溃(演示加分镜头)
7 正常删除一个联系人 提示"已删除 xx"
8 退出 → 重新打开 → 查看 数据还在(演示持久化)

彩排时对着手机录一遍,回头看成片就能发现自己哪里卡壳、哪里讲得绕。录完删掉就行,这视频是练给自己看的。

最后的检查(明天的包里带什么):① 代码文件(contacts.py)在;② contacts.json 里预存好 2~3 个演示数据;③ Git 提交记录在;④ 心里默背三个问题的回答框架。全部打勾,今晚安心睡。

演示数据提前存好:别明天现场添加,万一输入慢、手滑,2 分钟演示时间就没了。故意输错的演示只做一次(选"输入 abc"这个最有戏剧效果的),别每个功能都输错一遍,浪费时间。彩排时别自欺欺人:哪一步没跑通,立刻回去修,而不是"明天应该没问题"。

随堂练习

  1. 练习一:功能补全与查漏(25 分钟)——打开你的代码,对照上面的自查清单逐条检查:缺注释补注释、缺 try 补 try、缺 save 补 save。每改一处运行测试一次。完成后跑一遍完整流程:添加 → 查看 → 搜索 → 删除 → 退出 → 重启 → 数据还在。

  2. 练习二:加一个进阶功能(25 分钟)——从四个进阶功能里选一个做(按能力选):修改联系人(较简单)→ 数据校验(较简单)→ 统计/排序(较难,但最出彩)→ 全做更好。做完挂到菜单上(注意菜单编号顺延、退出编号改成最大的数),并测试通过。做不出来的地方用"四要素"问 AI。

  3. 练习三:三个问题讲项目(15 分钟)——不看代码,用上面的回答框架向同桌讲一遍项目,请他按"老师提问"的角度考你 2 个问题(如"为什么这里用 try")。讲不出来的地方记下来,回代码找答案。

  4. 练习四:Git 提交 + 彩排(15 分钟)——在项目文件夹执行 git init(如未建)→ git add .git commit -m "完成通讯录项目",用 git status 确认提交成功。然后按彩排清单完整走一遍演示流程。

  5. 进阶题(代码交给 AI 体检)——用下面的"代码检查"提示词,把完整代码发给 AI 做一次体检,把 AI 找到的问题分类:哪些必须修(bug)、哪些是建议(风格)。只修"必须修"的,别让 AI 把代码改得面目全非——AI 提建议,你拍板

AI 协作提示词:最终代码检查

场景:项目完成后的最终代码检查
提示词:我是 Python 初学者,做了一个通讯录项目,现在功能有:
添加、查看、搜索、删除联系人,数据存 contacts.json 文件。
请帮我做一次代码检查:
1. 找出潜在 bug(比如漏掉的异常处理、边界问题);
2. 给代码加上清晰的注释;
3. 指出哪些地方可以写得更好(不用改功能)。
请分点列出问题,每条说明严重程度(必须修 / 建议改),
最后再给出修改建议,但不要直接给我一整套新代码。
我的代码如下:
<粘贴你的完整代码>

报错速查

报错信息 什么意思 怎么办
git: command not found Git 没装或没在 PATH 重装 Git 并勾选 Add to PATH,重新打开终端
fatal: not a git repository 不在 Git 仓库里 git init(或 cd 到项目文件夹再操作)
error: pathspec 'xxx.py' did not match git add 的文件不存在 检查文件名拼写、当前目录对不对(用 ls 看)
菜单新增功能后编号乱 加了新功能没理顺编号 从 1 数到最大的数,逐项核对菜单文字和 elif 分支
修改功能改完没变化 忘了保存 检查 save_contacts(contacts) 有没有加;确认改的是 contacts[num-1][...] 而不是新建了字典
SyntaxError 突然多了很多 可能动坏了缩进 检查最近改动的函数,统一 4 空格缩进,别混用 Tab

周六的测验与答辩说明

最后,把周六的事交代清楚,你今晚就按这个冲刺。

周测形式:上机操作题,考动手,不考背诵。周六是阶段测验 + 项目答辩,全部上机。现场独立完成通讯录 / 记账本的核心功能:添加、查看、搜索、删除、保存。

考核允许与禁止:

  • 允许:查自己的笔记、问 AI(AI 只能当助手,不能代考——考核的核心是"你会不会用 AI + 看懂代码")
  • 禁止:抄同桌的代码、让 AI 一次性生成整套代码后直接提交

评分重点(100 分分布):

占比 评分维度 怎么得分
60% 功能是否完整跑通 添加 → 查看 → 搜索 → 删除 → 数据保存,一步步做出来
20% 代码是否看得懂、能讲清 每个函数能说出"干什么、参数是什么、返回什么"
20% AI 使用是否熟练、会追问修改 报错会用三步法问 AI,会追问"为什么这么改"

加分项(有就加):数据校验(电话 11 位)、修改联系人功能、界面提示友好(输错有提示不崩溃)、能讲清每个函数的作用。

答辩流程(每人 5 分钟):

  1. 演示项目(2 分钟):跑一遍添加、查看、搜索、删除,展示数据保存
  2. 讲解设计(2 分钟):数据结构怎么设计的、最难的部分怎么解决的
  3. 老师提问(1 分钟):一般问"这个函数是干嘛的""为什么这里用 try"

未通过怎么办:周测未通过的同学,在周末安排补测(一般是周日下午或晚上,具体时间听老师通知)。补测形式相同,考前多练几遍完整流程:把"添加 → 查看 → 搜索 → 删除 → 重启数据还在"这条链路跑到闭着眼睛都能敲出来。补测通过同样算过关,不影响进入阶段二。

今晚最后冲刺:① 用彩排清单完整走一遍流程;② 对着"老师常问的三个问题"自问自答;③ 把 contacts.json 预存好 2~3 个演示数据;④ 早点睡,明天状态最重要。