
Tyree前几天把他那个猜数字游戏邀同学一来一起玩。同学玩了一局,说:“每次结束都要重新运行程序,太麻烦了。能不能玩完一局自动问‘再来一局’?” Tyree一拍大腿:“对啊,我怎么没想到!”
于是他在程序后面加了while True和input("再玩一局?"),解决了这个问题。但他看着代码有点不舒服:整个游戏挤在一起,上百行,想改一个地方要翻半天。
他跑来跟我说:“妈妈,你看我这代码,乱七八糟的。能不能把游戏拆成几块?比如‘输入数字’是一块,‘判断大小’是一块,‘记录成绩’是一块……这样我以后想改‘猜数字范围’或者‘限制次数’,只改那一块就行了。”
我说:“这不就是我们之前学的函数吗?你把每个独立功能写成一个函数,主程序只负责调用它们。以后你想加‘难度选择’或者‘历史最佳成绩’,直接加函数,不乱动别的。”
Tyree听完,花了一个晚上,把游戏拆成了四个函数:
get_valid_input(专门管输入)、check_guess(专门管比较)、play_one_round(管一局流程)、main(管整体)。
改完后他兴奋地说:“现在我的代码像乐高积木一样,想拆就拆,想拼就拼!”
今天我们就跟着Tyree的思路,把猜数字游戏彻底用函数重构一遍。
01
先写“管输入的”:get_valid_input
这个函数只管一件事:死皮赖脸地问你要一个合格的数字,你要是不给数字,或者给的范围不对,它就跟你没完。

这个函数,名字叫
get_valid_input,翻译过来就是“拿到一个有效的输入”。
它干的事就只有一个:死皮赖脸地问你要数字,你要是不给数字,或者给的范围不对,它就跟你没完。
里面那个 while True 是个死循环,意思就是“你不给我对的,我就不走了”。
Tyree第一次看到这个写法,说:“这不是耍赖吗?”我说:“对,编程里这叫‘防御式编程’,就是防着用户瞎输入。”
第一关:isdigit()。用户有可能输入“abc”或者“一二三”,isdigit() 就返回 False,然后打印“输入的不是数字”,接着 continue 跳回去重新问。
第二关:检查范围。比如你要求1到100,他输入200,程序就说“数字超出范围”,然后 continue 回去重新问。
最后,啥都合格了,
return guess。这一句一执行,函数就结束了,那个死循环终于被打破。
“为什么不用 break?”
“return 不仅跳出循环,还顺便把值带回去。break 只是跳出循环,你还得再写一行 return,多麻烦。″
02
再写“管判断的”:check_guess
这个函数只干一件事:把你猜的数字跟秘密数字比一下,然后告诉你结果。

就这么几行,一点都不复杂。但Tyree问了一个好问题:“为什么不用 print 直接打印,非得用 return 把结果送回去?”
我给他打了个比方:print 就像你在屋里大喊一声“猜中了”,外面的人听见了,但也就听个响。
return 就像你走到那人面前,把一张纸条递给他,纸条上写着“猜中了”。他拿到纸条,可以念出来,也可以存起来,甚至转交给别人。是不是更灵活?
后来Tyree想给猜数字游戏加一个功能:猜错的时候等1秒再提示,制造紧张感。
如果用的是 print,他就得在 check_guess 函数里面改;如果用的是 return,他只需要在外面调用 check_guess 的地方,拿到结果之后,再决定什么时候打印。
03
写“管一局游戏的”:play_one_round
这个函数负责一局的完整流程:生成一个秘密数字、计时、反复问用户猜、直到猜中或者次数用完,最后返回这一局用了多少次、多少秒。

这个函数稍微长一点,但拆开看就清楚了。
第一行
secret = random.randint(...) 生成一个随机数,这是游戏的核心。
start = time.time() 掐表开始。
while attempts < max_attempts: 循环,只要没到次数上限就一直猜。
循环里面调用了之前写的两个函数:get_valid_input 拿数字,check_guess 比较结果。
如果猜中了,break 跳出循环,后面那句“机会用完了”就不会执行。
如果次数用完了还没猜中,就打印正确答案。
最后 end = time.time() 掐表结束,算出用时 elapsed。
函数把 attempts(猜的次数)和 elapsed(用的秒数)一起返回。
Tyree测试这个函数的时候,有一次运气爆棚,第一次就猜中了,只用了1次和2.3秒。
他兴奋地拿笔记下来:“最高纪录:1次猜中!”
后来他还故意把 max_attempts 改成1,想看看能不能每次都一次猜中,结果发现概率只有百分之一,他说:“果然不是我能控制的。”
注意这里几个参数
min_num=1,max_num=100, max_attempts=5 都给了默认值。
这样以后你想玩不同范围或者不同次数,直接传新值就行,不改原来的代码。Tyree说:“默认参数就像‘懒人模式’,大部分时候直接用,特殊时候再自己调。”
04
最后写“管全局的”:main
这个函数负责整个程序怎么跑:启动游戏、问你要不要再来一局、记录历史最佳成绩。

best_attempts = float('inf') 这行看起来有点怪。float('inf') 是正无穷大,意思就是“现在还没有成绩,谁都比这个好”。
第一次玩完之后,成绩肯定比无穷大小,所以就会被记下来。
Tyree刚开始写的时候用 best_attempts = 0,结果后面比较 attempts < 0 永远不成立,最佳成绩一直是0。
他挠头说:“怎么不更新?”我告诉他:“0比任何正数都小,你第一次猜了5次,5不小于0,当然不更新。”他改成无穷大之后就好了。
round_num 就是个计数器,显示第几局,没什么实际作用,但看着舒服。
again = input(...).strip().lower() 这一行处理用户的输入。
.strip() 去掉首尾空格,.lower() 转成小写。这样用户输入“Y ”、“y”、“YES”都能正确识别为“y”。
Tyree一开始只判断 again == 'y',结果同学输入“Y”就不行,他加了这两招之后就通吃了。
最后,如果用户输入不是 y,break 跳出循环,游戏结束。
05
拼起来:完整代码
把上面所有函数放在一个文件里,最上面导入模块,最后加上 if __name__ == "__main__":。

07
今天我们学了什么?
拆分函数一个函数只干一件事,好改、好测、好复用。
return把结果递出来,外面想怎么用就怎么用默认参数调用时可以偷懒不传,省事。
main()把程序的主逻辑放一起,一看就懂。
if __name__ == "__main__"防止被别人当模块导入时自动运行。
好了,今天课程就讲到这。
下一课开始,我们要玩点有意思的了:文件读写、字符串进阶,还要做三体名言抽卡、水熊虫生存模拟。Tyree已经等不及了。
————热门推荐————
自学编程第19课:Python time模块——让程序学会“等待”和“计时”
自学编程第16课:Python函数的偷懒技巧——默认参数、关键字参数、可变参数
自学编程第7课:turtle画图入门(画一个正方形,五角形,螺旋形,三角形)
自学编程第2课:用input让电脑问你名字(做一个打招呼程序)
自学编程第一步:安装Python和Thonny(零基础图文教程)
(本系列教程每天更新,欢迎关注收藏)