第27章-项目开发一:菜单框架与核心功能


昨天的施工图画完了,今天正式开工。这一天的目标只有一个字:。哪怕程序丑、功能少,只要打开能显示菜单、能添加联系人、能查看、能退出,你就成功了一半。

很多同学一上来就想把"添加、查看、搜索、删除、保存"一次全写完,结果东错一块西错一块,一整天都在跟报错搏斗。别这么干。今天不碰 JSON、不碰异常处理、不碰搜索删除,就把"菜单 + 添加 + 查看 + 退出"这四件小事做扎实。这版跑通了,后面三天都是在它上面加功能——地基打得牢,盖楼才不慌。

先跑通,再完善

这个做法有个正经名字,叫 MVP(Minimum Viable Product),最小可运行版本。先做一个"功能最少的能运行版本",跑起来了再往上加。

想想你学骑自行车:没有人是先学会"边骑车边单手喝水"再开始骑的,都是先能歪歪扭扭骑直线,再慢慢学转弯、刹车、单手。写程序一模一样。本周的节奏就是按这个排的:

阶段 内容 状态
今天(MVP) 菜单 + 添加 + 查看 + 退出 数据只在内存,关掉就没了
明天 加搜索 + 删除 功能 4 个
后天 加 JSON 保存 + 异常处理 数据能存住,输错不崩
周五 加进阶功能 + 完善 完整作品

所以今天千万别把 jsontry/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_contactsreturn 表示"空列表就不用往下看了"。

函数第一行的 """添加一个联系人""" 这种写法叫 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_contactshow_contacts 必须已经定义好了。把调用放最后,保证所有函数都"先定义、后使用"。忘了最后一行 main(),程序什么都不会发生——只定义了函数没人调用,这也是新手常见问题。

看 main 里的结构:它没有具体逻辑,全是 if choice == "1": add_contact(contacts) 这种"分发"语句——选什么就调什么,代码像菜单一样一目了然。这就是"一个功能一个函数"在 main 里的体现。

两个容易翻车的点。第一,函数里用 contacts 一定要通过参数传进来,别在函数里写 contacts = [] 重新赋值——那会新建一个局部空列表,往里面加的人外面根本看不到。第二,print 前面加 \n(换行)是为了让菜单和上一次输出分开,界面清爽,这是"提示友好"的加分细节,可以养成习惯。

跑通之后可以做个小实验:把最后一行 main() 注释掉再运行,看看会发生什么——程序安安静静地什么都不干。这个实验做一次,你就再也不会忘写 main() 了。

随堂练习

  1. 练习一:照抄运行(15 分钟)——把上面最小可跑版的完整代码照抄到自己的文件里(contacts.py,手动敲,别复制粘贴),运行,按这个流程测一遍:添加 2 个联系人(小明、小红)→ 查看 → 输入 9 看看提示 → 退出。每跑一步对照运行结果,确认和自己预期一致。

  2. 练习二:理解三问(10 分钟)——合上代码,口头回答同桌这三个问题(答不上来就回去看代码):① while True 在干嘛?为什么选 3 能退出?② 联系人为什么用"字典列表"存?③ 如果把 enumerate(contacts, 1)1 删掉,显示会变成什么样?(答案:编号从 0 开始)

  3. 练习三:独立敲一遍(20 分钟)——新建一个空白文件 contacts2.py不看任何参考,凭记忆把完整程序敲一遍。敲到卡壳的地方(比如忘了 enumerate 怎么写),先自己猜,实在想不起来再看书,然后把这个薄弱点记下来。能独立敲通,今天就算真的学会了。

  4. 进阶题(可选)——给 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: 那句。)