昨天的施工图画完了,今天正式开工。这一天的目标只有一个字:跑。哪怕程序丑、功能少,只要打开能显示菜单、能添加联系人、能查看、能退出,你就成功了一半。
很多同学一上来就想把"添加、查看、搜索、删除、保存"一次全写完,结果东错一块西错一块,一整天都在跟报错搏斗。别这么干。今天不碰 JSON、不碰异常处理、不碰搜索删除,就把"菜单 + 添加 + 查看 + 退出"这四件小事做扎实。这版跑通了,后面三天都是在它上面加功能——地基打得牢,盖楼才不慌。
先跑通,再完善
这个做法有个正经名字,叫 MVP(Minimum Viable Product),最小可运行版本。先做一个"功能最少的能运行版本",跑起来了再往上加。
想想你学骑自行车:没有人是先学会"边骑车边单手喝水"再开始骑的,都是先能歪歪扭扭骑直线,再慢慢学转弯、刹车、单手。写程序一模一样。本周的节奏就是按这个排的:
| 阶段 | 内容 | 状态 |
|---|---|---|
| 今天(MVP) | 菜单 + 添加 + 查看 + 退出 | 数据只在内存,关掉就没了 |
| 明天 | 加搜索 + 删除 | 功能 4 个 |
| 后天 | 加 JSON 保存 + 异常处理 | 数据能存住,输错不崩 |
| 周五 | 加进阶功能 + 完善 | 完整作品 |
所以今天千万别把 json 和 try/except 提前写进去——代码一多,报错都不知道出在哪。分步走,每步都能验证,是最稳的节奏。"内存版"也不是没用:它让你先练熟"菜单 + 数据结构"这两块核心,文件只是把数据换个地方放而已。
今天的"完成标准"就三条:打开能出菜单;能添加并查看;选退出能正常结束。这三条打勾,今天就过关,别再多加功能——加多了反而打乱明天的节奏。
程序骨架:while True 菜单循环
一个能反复操作的程序,骨架长这样:循环菜单 = 程序永远不结束,反复问用户"接下来要干嘛"。就像便利店店员一直站着,来一个顾客服务一个,没人来就继续等着——这就是 while True。
# 骨架:一个永不停歇的循环
while True:
print("\n1.添加联系人 2.查看全部 3.退出")
choice = input("请选择:")
if choice == "1":
print("执行添加功能")
elif choice == "2":
print("执行查看功能")
elif choice == "3":
break # 用户选 3,跳出循环,程序结束
else:
print("输入无效,重新选择")
输出长这样:
1.添加联系人 2.查看全部 3.退出
请选择:1
执行添加功能
1.添加联系人 2.查看全部 3.退出
请选择:9
输入无效,重新选择
1.添加联系人 2.查看全部 3.退出
请选择:3
(程序结束)
逐行拆开看:
| 代码 | 在干嘛 |
|---|---|
while True: |
条件永远是"真",循环永远不结束,一直重复 |
choice = input(...) |
停下等用户打字,把输入存进 choice |
if choice == "1": |
注意!input 返回的是字符串,所以要和字符串 "1" 比较,不是数字 1 |
elif |
"否则如果",按顺序判断,前面的不成立才看后面的 |
break |
立即跳出整个循环,程序走到结尾自然结束 |
else: |
上面所有条件都不满足(比如输入了 9 或 abc) |
最大的坑就是 input 返回字符串:choice == 1(数字)永远不成立,必须写 choice == "1"(带引号)。这里很容易写反,是新手最懵的地方。另外每个 if / elif 后面别忘冒号 :,缩进统一 4 个空格,混用空格和 Tab 会报 IndentationError。
为什么用 while 而不是 for?for 循环是"数数",数完就停;while 是"条件成立就一直跑",True 永远成立,正好符合菜单"不知道用户会用多少次"的场景。还有一点:选 3 用 break 跳出,不是 exit() 也不是让程序自然"跑完"——break 只是"离开菜单这个循环",后面还能继续执行别的代码(以后写登录程序就靠这个思路)。
这个小骨架今天会一直长:明天给它挂上搜索和删除,后天挂上文件读写,大后天挂上修改和校验——但循环和分支的结构一个字都不用改。地基先打对,后面都是添砖。
一个功能一个函数
函数 = 把一段代码打包成"一个动作"。像厨房里备好的料:要葱花就抓一把,要姜末就抓一把,不用每次现切。项目里每个功能(添加、查看、搜索、删除)都打成独立函数,好处有三个:main() 里一眼看到"添加、查看、退出",不用读几百行细节;搜索有 bug,只改 search_contact 一个函数,不会碰乱别的;每个函数只干一件事,方便一边写一边测试。
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 函数名(参数): 是定义;参数是函数干活需要的"原材料",这里传 contacts 是因为两个函数都要动同一个列表,把列表传进来,函数才能操作它;return 把结果"递出来",show_contacts 里 return 表示"空列表就不用往下看了"。
函数第一行的 """添加一个联系人""" 这种写法叫 docstring(文档字符串),是给函数写一句"功能说明书"。现在你可能觉得多此一举,但周五的对照自查清单里专门有这一项,早点养成习惯。
为什么参数里必须传 contacts,而不是在函数里再建一个空列表?因为你往"函数里的列表"加人,外面的列表啥也不知道。列表是可变对象,函数内部 contacts.append(...) 改的就是外面那个列表本身,不需要 return 传回——这是"引用传递",以后学指针时还会见面。
show_contacts 里先判断 if not contacts:空列表直接提示并 return 提前结束,避免后面代码白跑或出错。not contacts 就是"列表为空"(空列表是 False)。
用 enumerate 给联系人编上号
"查看全部要带编号"——编号从哪来?用 enumerate:一边数数一边取东西,就像点名,老师念一个名字报一个号:1 号小明、2 号小红。它把列表的下标(0、1、2…)变成人话编号(1、2、3…)。
contacts = [
{"name": "小明", "phone": "13800000000"},
{"name": "小红", "phone": "13900000000"},
]
# 不用 enumerate 的写法:自己数
i = 1
for c in contacts:
print(f"{i}. {c['name']} - {c['phone']}")
i = i + 1
# 用 enumerate 的写法:自动数(推荐)
for i, c in enumerate(contacts, 1):
print(f"{i}. {c['name']} - {c['phone']}")
两种写法输出一样:
1. 小明 - 13800000000
2. 小红 - 13900000000
enumerate(contacts, 1) 的意思是遍历 contacts、从 1 开始编号,第二个参数是起始号,不写默认从 0 开始;for i, c in ... 每次循环拿到两个值——一个收号、一个收货,两个变量之间是逗号,别写成 for i c in ...。
编号从 1 开始是因为"人话"没有 0 号:用户说"删第 2 个"是指小红,不是小明。这也为明天的"按编号删除"埋了伏笔:编号 = 下标 + 1。
还有个小细节,f-string 里的大括号:f"{i}. {c['name']} - {c['phone']}" 中 c['name'] 用单引号包键名,外面 f-string 是双引号,两者不冲突;如果你外层也写单引号,就会语法错误。
练熟 enumerate 的另一个理由是:它几乎是所有"列表展示"类功能的标配,以后写购物车、成绩单、订单列表,全是这个套路。今天花十分钟把它揉进肌肉记忆,比以后每次翻笔记划算。
拼装 main():今天的最小可跑版
main() 是总指挥,负责把所有功能串起来:先备好数据(contacts = []),再进入菜单循环,根据用户选择调用对应函数。它就像饭店的大堂经理:客人说要吃面(选 1),就喊后厨(add_contact)做面。
今天完整的最小可跑版,抄下来运行一遍,理解之后自己敲一遍:
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 main():
while True:
print("\n1.添加联系人 2.查看全部 3.退出")
choice = input("请选择:")
if choice == "1":
add_contact(contacts)
elif choice == "2":
show_contacts(contacts)
elif choice == "3":
break
else:
print("输入无效,重新选择")
main() # 启动程序(调用总指挥)
跑起来的效果:
1.添加联系人 2.查看全部 3.退出
请选择:1
姓名:小明
电话:13800000000
添加成功!
1.添加联系人 2.查看全部 3.退出
请选择:2
1. 小明 - 13800000000
1.添加联系人 2.查看全部 3.退出
请选择:3
(程序结束)
为什么 main() 要放在文件最后?Python 按行从上往下执行:main() 被调用时,它里面用到的 add_contact、show_contacts 必须已经定义好了。把调用放最后,保证所有函数都"先定义、后使用"。忘了最后一行 main(),程序什么都不会发生——只定义了函数没人调用,这也是新手常见问题。
看 main 里的结构:它没有具体逻辑,全是 if choice == "1": add_contact(contacts) 这种"分发"语句——选什么就调什么,代码像菜单一样一目了然。这就是"一个功能一个函数"在 main 里的体现。
两个容易翻车的点。第一,函数里用 contacts 一定要通过参数传进来,别在函数里写 contacts = [] 重新赋值——那会新建一个局部空列表,往里面加的人外面根本看不到。第二,print 前面加 \n(换行)是为了让菜单和上一次输出分开,界面清爽,这是"提示友好"的加分细节,可以养成习惯。
跑通之后可以做个小实验:把最后一行 main() 注释掉再运行,看看会发生什么——程序安安静静地什么都不干。这个实验做一次,你就再也不会忘写 main() 了。
随堂练习
-
练习一:照抄运行(15 分钟)——把上面最小可跑版的完整代码照抄到自己的文件里(
contacts.py,手动敲,别复制粘贴),运行,按这个流程测一遍:添加 2 个联系人(小明、小红)→ 查看 → 输入 9 看看提示 → 退出。每跑一步对照运行结果,确认和自己预期一致。 -
练习二:理解三问(10 分钟)——合上代码,口头回答同桌这三个问题(答不上来就回去看代码):①
while True在干嘛?为什么选 3 能退出?② 联系人为什么用"字典列表"存?③ 如果把enumerate(contacts, 1)的1删掉,显示会变成什么样?(答案:编号从 0 开始) -
练习三:独立敲一遍(20 分钟)——新建一个空白文件
contacts2.py,不看任何参考,凭记忆把完整程序敲一遍。敲到卡壳的地方(比如忘了 enumerate 怎么写),先自己猜,实在想不起来再看书,然后把这个薄弱点记下来。能独立敲通,今天就算真的学会了。 -
★ 进阶题(可选)——给
show_contacts后面加一段:查看完联系人后,额外打印"共有 N 个联系人"(提示:len(contacts))。这是周五加分项"统计信息"的雏形,现在做出来,周五就轻松了。
练习三独立敲通后,把刚才卡壳的地方用红笔标在笔记上,明天写代码前先看一遍,你会发现自己进步很快。
让 AI 帮你排错
程序跑不起来或者报错,用四要素问 AI:身份 + 背景 + 问题 + 目标。下面这段直接复制,把尖括号里的内容换成你自己的:
场景:程序跑不起来 / 报错,用四要素提问(身份 + 背景 + 问题 + 目标)
提示词:我是 Python 初学者,正在写通讯录项目的菜单循环,
功能是输入 1 添加、2 查看、3 退出。运行时报错了。
报错原文:
<粘贴你的报错信息>
我的代码如下:
<粘贴你的代码>
请用大白话告诉我:1. 错在哪一行,为什么错;2. 怎么改。
先别给我完整代码,先让我自己试着改。
报错速查
| 报错信息 | 什么意思 | 怎么办 |
|---|---|---|
SyntaxError: invalid syntax |
语法错误,某行写错了 | 查引号、括号、冒号;看看是不是把 elif 写成 else if 了 |
IndentationError: expected an indented block |
该缩进的地方没缩进 | if / while / def 下一行必须缩进 4 个空格,且全文件统一 |
NameError: name 'contacts' is not defined |
变量没定义 | 检查拼写;确认 contacts = [] 写在函数外面且在使用之前 |
NameError: name 'main' is not defined |
没定义 main | 是不是把 def main(): 写错了,或漏了最后的 main() 调用 |
| 选 1 没反应 / 选 3 退不出 | 判断条件写错 | 检查是不是写了 choice == 1(少引号),input 返回的是字符串,必须 choice == "1" |
收个尾
今天跑通的这一版,是本周的地基:菜单循环、一个功能一个函数、enumerate 编号、main 分发。你把"添加 → 查看 → 退出"这条最小链路敲通,说明骨架已经立起来了。
明天加"搜索"和"删除"。核心思路你今天其实已经见过:搜索就是"遍历列表 + 判断关键字在不在字段里",删除就是"编号 = 下标 + 1,用 pop 删"。今天把字典和列表的遍历练熟,明天就是水到渠成。
临走想一个问题:用户选 2 查看时列表是空的,程序会打印什么?这个分支写在代码的哪一行?(答案在 show_contacts 开头的 if not contacts: 那句。)