昨天的程序能存人了,但还不会"找"、不会"删"。想想你手机通讯录的用法:记了几百个联系人之后,你不会一条条翻着找,而是直接搜名字;联系人不想要了,直接删掉。这两个功能,就是让程序从"能存"变成"能用"。
今天的代码不多,但套路非常典型:搜索 = 遍历 + 判断,删除 = 编号 + 下标换算。这两个套路几乎是所有管理系统(图书管理、库存、学生信息)的通用零件,这周练熟了,后面所有项目都直接复用。下午你还会第一次正经地用"三步法"问 AI 报错——别怕,报错是程序员最熟悉的陌生人。
今天的两个功能还有一个共同点:都要遍历——把联系人列表从头到尾看一遍。搜索是边看边找,删除是看准了下手。"遍历"这个动作,今天会贯穿始终。
模糊搜索:用 in 判断"包含"
模糊搜索 = 输入一部分,也能搜到完整结果。你输入"小",能搜出"小明""小红";输入"138",能搜出所有电话以 138 开头的联系人。靠的是一个运算符:in。
in 有两种用法,今天都用得上:
| 用法 | 含义 | 示例 |
|---|---|---|
| 字符串里包含 | "小" 在不在 "小明" 里面 | "小" in "小明" → True |
| 列表里包含 | 元素在不在列表里 | "小明" in ["小明", "小红"] → True |
# 字符串用 in:判断"关键字"是不是"字符串"的一部分
print("小" in "小明") # True ("小明"里包含"小")
print("张" in "小明") # False ("小明"里没有"张")
print("138" in "13800000000") # True(电话里包含 138)
# 对比:== 是"完全相等",in 是"包含在里面"
print("小" == "小明") # False (不一样长)
print("小" in "小明") # True (但确实在里面)
为什么搜索要用 in 而不是 ==?需求写的是"模糊搜索"。用 == 的话,用户必须把名字完整打对"小明"才搜得到,打"小"就搜不到——不符合使用习惯。in 只要"包含"就算命中,这正是"模糊"的意思。
写的时候注意顺序:in 的两边都是字符串,kw in c["name"] 判断"关键字在不在姓名里",短的在长的里面,写反成 c["name"] in kw 意思就完全反了。键名也别写错:c["name"] 和 c["phone"] 的键名要和添加时统一,否则 KeyError。想更智能可以加 kw.lower() in c["name"].lower() 让搜索不区分大小写,这是进阶玩法,今天不做不强求。
搜索函数:found 标志位
搜索函数里有个关键角色叫 found(标志位),它的作用是记录"找到没"。为什么要它?因为"有没有搜到"这个结论,要等整个循环跑完才能下——你没法在循环里提前知道后面还有没有匹配的。就像老师找迟到的学生,得把全班点一遍名才能说"今天没人迟到"。
def search_contact(contacts):
kw = input("输入要搜索的姓名或电话:")
found = False # 1. 先假设没找到
for c in contacts:
if kw in c["name"] or kw in c["phone"]:
print(f"{c['name']} - {c['phone']}") # 2. 找到了就打印
found = True # 3. 记号改成"找到了"
if not found: # 4. 循环完还标记没找到
print("没找到")
运行效果:
输入要搜索的姓名或电话:小
小明 - 13800000000
小红 - 13900000000
输入要搜索的姓名或电话:王
没找到
kw in c["name"] or kw in c["phone"] 表示"姓名里包含 或 电话里包含",两个条件满足一个就算命中,这就实现了"按姓名或电话搜索"。
最容易漏的就是 found 标志位:没有它,搜不到时程序什么都不打印,用户以为程序死了。加一句 print("没找到") 就是"提示友好"。也别用计数器替代标志位——count += 1 数得明白,但语义上没有 found 清晰,打印重复信息还更容易出错。最后一点:多个匹配打印多行是正常现象,模糊搜索本来就可能命中多条,一次打印一行即可,不用把它们存起来。
这个"先假设没找到、找到了再翻盘"的思路,以后还会反复出现:查表、找文件、判断登录用户存不存在,都是同一个套路。今天把它吃透,后面就是白捡。
删除:编号 = 下标 + 1
删除的流程是:先让用户看到编号,用户报编号,程序把"编号"换算成"下标"再删。核心换算只有一句:编号 = 下标 + 1,所以下标 = 编号 - 1。
为什么绕这一下?因为 Python 列表的下标从 0 开始(contacts[0] 是第一个),但人习惯从 1 数起("删第 1 个")。昨天 enumerate(contacts, 1) 显示编号时,编号比下标大 1;今天删除时,就要把编号减 1 换回下标:
contacts = [
{"name": "小明", "phone": "13800000000"}, # 下标 0,显示编号 1
{"name": "小红", "phone": "13900000000"}, # 下标 1,显示编号 2
]
num = 2 # 用户说:删第 2 个(小红)
index = num - 1 # 换算成下标:2 - 1 = 1
removed = contacts.pop(index) # 删下标 1 的元素
print(f"已删除 {removed['name']}") # 输出:已删除 小红
print(contacts) # 只剩小明了
pop(index) 删除指定下标的元素,并且把被删的元素"递出来"(所以能用 removed 接住,打印"已删除谁")。它和"删完就没了"的 del 不同:del contacts[0] 只删不返回;pop 删完还告诉你删的是什么。这里需要告诉用户"删了谁",所以用 pop 更合适。
编号和下标搞混是最典型的坑:用户说 1,你直接 pop(1) 会删掉第二个,必须 pop(num - 1)。pop 的参数是下标,不是编号,也不是联系人姓名。另外,列表空了或者编号越界时,pop 会直接报错(IndexError),所以删除前必须先检查——这就引出下一个知识点。
边界检查:动手之前先确认合不合法
边界检查 = 动手之前先确认"合不合法",就像过马路前先看红绿灯。删除前要检查两件事:列表是不是空的(空列表没得删,直接提示返回);编号在不在范围内(1 <= num <= len(contacts),防止用户输入 0、负数或超出列表长度的数)。
def delete_contact(contacts):
show_contacts(contacts) # 先把联系人带编号显示出来
if not contacts: # 检查 1:空的就没得删
return
num = int(input("输入要删除的编号:"))
if 1 <= num <= len(contacts): # 检查 2:编号在有效范围内
removed = contacts.pop(num - 1)
print(f"已删除 {removed['name']}")
else:
print("编号无效")
运行效果:
1. 小明 - 13800000000
2. 小红 - 13900000000
输入要删除的编号:9
编号无效
输入要删除的编号:2
已删除 小红
1 <= num <= len(contacts) 是 Python 的"链式比较",等价于 num >= 1 and num <= len(contacts),连起来读:"编号至少是 1,且最多是联系人总数"。注意 len(contacts) 是动态的——列表越删越短,范围也跟着变。
不检查会怎样?输入 9 会触发 IndexError: pop index out of range,程序直接崩溃退出。检查之后,程序只会礼貌地说"编号无效",然后继续等用户操作——这是"程序摔不坏"的第一步,明天还会学更系统的异常处理。
你可能会想:崩一次就崩一次呗,重启不就好了。但想想使用场景——用户录了一上午的数据,一次手滑全没了,他还会再用这个程序吗?边界检查省的就是这种"前功尽弃"。
三个小提醒。检查顺序别反:先 show_contacts 让用户看到编号,再检查空列表(列表空了,show_contacts 会打印"通讯录是空的",用户自然知道没得删)。int(input(...)) 有个隐患——用户输入"abc"时 int 会报 ValueError 崩溃,今天先把这个坑记下来,明天用 try/except 正式解决它。还有,删的是"编号"对应的元素,不是"搜出来的某个名字",删除流程和搜索流程是分开的,别混在一起。
菜单从 3 项扩到 5 项
功能多了,菜单也要跟着变:把新函数挂到新的编号上,退出挪到最后一个。改菜单是个"细心活",编号必须连续、不能跳号。
def main():
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("输入无效,重新选择")
对照昨天的 3 项菜单,今天的改动就三处:菜单文字从 3 项变 5 项,退出变成 5;新增 elif choice == "3":(搜索)和 elif choice == "4":(删除)两个分支;退出口从 elif choice == "3": 改成 elif choice == "5":。
检查要点:菜单显示"1 添加 2 查看 3 搜索 4 删除 5 退出",那么 if/elif 分支也必须从 1 写到 5,一一对应。常见的低级错误是菜单写了 5 项、分支却漏了 elif choice == "3",导致"显示有搜索、选了没反应"。新增功能时最容易犯的错是忘了改退出的编号——菜单变成 5 项后,break 必须挂在 choice == "5" 上,否则用户按 5 程序没反应。每个新增分支的缩进和冒号也检查一遍:elif 与 if 对齐,冒号结尾。
报错就用三步法问 AI
今天开始正式用"三步法"问 AI,这是报错时的固定套路:① 说清楚自己在做什么项目、哪一步;② 贴上完整的报错原文;③ 贴上出错的代码,并说明"我是初学者,要大白话"。别把几行报错记成"报错了"三个字就丢给 AI——AI 再聪明也不知道你错在哪。
我在运行通讯录项目时输入删除编号后报错。
报错原文:
<粘贴报错,比如:ValueError: invalid literal for int() with base 10: 'abc'>
我的代码如下:
<粘贴相关代码>
我是初学者,请用大白话解释原因,并给出修改后的代码。
好的提问和差的提问,区别一眼就能看出来:
| 差的提问 | 好的提问 |
|---|---|
| "我的程序报错了,怎么办" | 交代项目背景 + 贴报错原文 + 贴代码 + 要大白话 |
| 只贴报错不贴代码 | 报错 + 代码一起贴,AI 才能定位 |
| "直接给我完整代码" | "先讲原因,让我自己试着改"(更能练能力) |
拿到 AI 的回答后别直接复制:先看它解释的原因,再理解它的修改,最后自己动手改。AI 是教练不是替身,答辩时老师会问"这段代码你懂不懂"。
细节上注意:报错原文要完整复制,尤其是最后一行(真正的原因通常在那里)和它上面的几行;贴代码时贴出错的函数就够了,不用把整个项目几百行全贴进去;让 AI 说大白话 + 分点,回答质量会高很多,问"为什么这么改"可以继续追问,这是第五周学过的"追问"技巧。
周三完整版:搜索 + 删除合体
把周一到周三的代码合体,就是"功能 4 个"的完整程序。抄进你自己的文件里运行一遍,重点看新加的两块是怎么和菜单咬合的。
contacts = []
def add_contact(contacts):
name = input("姓名:")
phone = input("电话:")
contacts.append({"name": name, "phone": phone})
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
num = int(input("输入要删除的编号:"))
if 1 <= num <= len(contacts):
removed = contacts.pop(num - 1)
print(f"已删除 {removed['name']}")
else:
print("编号无效")
def main():
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()
完整流程跑一遍:
1.添加 2.查看 3.搜索 4.删除 5.退出
请选择:1
姓名:小明
电话:13800000000
添加成功!
1.添加 2.查看 3.搜索 4.删除 5.退出
请选择:3
输入要搜索的姓名或电话:小
小明 - 13800000000
1.添加 2.查看 3.搜索 4.删除 5.退出
请选择:4
1. 小明 - 13800000000
输入要删除的编号:1
已删除 小明
跑通之后对照自查四点:① 菜单 5 项和分支 1~5 一一对应;② search_contact 有 found 标志位;③ delete_contact 有"空列表判断 + 编号范围判断";④ 搜索挂在 3、删除挂在 4。这四点和你的代码逐一比对,确认一致再往下走。
跑通后自己再加两个测试场景:搜一个不存在的关键字、删一个越界的编号,看看程序是不是都"礼貌回应"而不是崩掉。
这版还没有 JSON 持久化和异常处理(删编号输"abc"会崩溃),这是故意的——明天才加,今天先把逻辑练扎实,别提前把明天的代码抄进来打乱节奏。
另外提醒一句:明天加持久化时要动三个地方(main 读文件、添加后保存、删除后保存),如果今天的代码结构和你自己的不一致,明天改起来会乱。今天务必以你自己的代码为准对照整理,而不是让教材版本取代你的。
随堂练习
-
练习一:加上搜索功能(15 分钟)——在昨天的
contacts.py里加上search_contact函数,把菜单改成 5 项。测试用例:添加"小明 13800000000"和"小红 13900000000"→ 搜"小"应出来 2 条 → 搜"138"应出来 1 条 → 搜"王"应提示"没找到"。 -
练习二:加上删除功能(20 分钟)——再加上
delete_contact函数。测试用例:查看显示编号 → 删第 2 个 → 查看确认少了 → 删一个不存在的编号(如 9)→ 应该提示"编号无效"而不是崩溃。然后把两个函数都挂在菜单上跑通全流程:添加 → 查看 → 搜索 → 删除 → 退出。 -
练习三:讲给同桌听(10 分钟)——不看代码,口头回答:① 为什么删除用
num - 1当下标?②pop和"直接删"有什么区别?③ 搜索为什么要found标志位?讲不出来的地方,马上回代码里找答案。 -
★ 进阶题(记账本版)——如果你做记账本,搜索改为"按用途搜索"(
kw in c["note"]);再加一个加分功能"统计总收入支出":遍历records,把type为"收入"的amount加起来、支出也加起来,打印"总收入 X 元,总支出 Y 元"(提示:两个变量分别累加)。
让 AI 帮你排查删除报错
场景:运行删除功能时报错,用三步法排查
提示词:我在运行通讯录项目时输入删除编号后报错。
报错原文:
<粘贴报错原文,如:ValueError: invalid literal for int() with base 10: 'abc'>
相关代码如下:
<粘贴 delete_contact 函数代码>
我是初学者,请用大白话解释:1. 报错原因是什么;2. 我应该怎么改;
3. 以后怎么避免。请分点回答,先讲思路,再给修改后的完整函数。
报错速查
| 报错信息 | 什么意思 | 怎么办 |
|---|---|---|
IndexError: pop index out of range |
删除时下标越界 | 删除前加边界检查 1 <= num <= len(contacts),确认用了 pop(num - 1) 而不是 pop(num) |
ValueError: invalid literal for int() with base 10: 'abc' |
想把"abc"转成数字失败了 | 这是明天要学的:用 try/except 包住 int(input(...));今天先知道输入数字的规则 |
KeyError: 'phone' |
字典里没有 phone 键 | 检查添加联系人时键名是不是写成了别的,比如 "tel",键名全项目统一 |
| 菜单显示 5 项,选 3 没反应 | 分支漏写了 | 检查有没有 elif choice == "3": search_contact(contacts),以及退出是不是改成了 choice == "5" |
TypeError: 'in <string>' requires string as left operand |
in 左边不是字符串 | 检查 kw in c["name"],确认 kw 是 input 得到的字符串、键名没拼错 |
收个尾
今天给程序装上了"搜索"和"删除",管理系统的两个通用零件到手:模糊搜索 = 遍历 + in 判断,删除 = 编号换算下标 + pop。再加上 found 标志位和边界检查,程序的"礼貌"程度已经超过很多玩具程序了。
明天是"让程序脱胎换骨"的一天:数据存进 JSON 文件(退出重开数据还在)+ 用 try/except 让程序输错不崩。今天删除时报的 ValueError,明天就有正式解法了。晚上可以翻教材提前扫一眼 json 模块和 try 语句,明天听讲更轻松。
再想一个问题:为什么搜索不需要先检查"列表为空",而删除必须检查?(提示:搜空列表不会报错,只会"没找到";删空列表会直接 IndexError。)