很多刚接触 Python 的朋友都会卡在同一个地方语法看懂了循环和判断也背下来了但就是写不出一个能跑起来、有点意思的小东西。我自己当年也是这样对着教程敲了几十遍print(hello world)直到有人让我写一个猜数字游戏才第一次真正把input、while、if、random这几个东西串起来。所以这篇就围绕用 Python 制作一个简单的猜数字游戏这件事把完整代码、每一步的设计理由、以及我踩过的坑都摊开讲清楚。它适合刚学完基础语法想找个练手项目的人也适合想拿它当教学案例的老师和家长。读完你不仅能复制走一份能直接运行的代码还能明白为什么这么写、哪里容易出错、后面怎么把它改得更好玩。1. 为什么猜数字是新手最该先写的那个程序1.1 它把 Python 最核心的几块知识一次性串起来了猜数字这个游戏看起来简单但它其实是一个麻雀虽小五脏俱全的典型。你要生成一个随机数就得用random模块你要让玩家反复猜就得用while循环你要判断猜大还是猜小就得用if-elif-else分支你要接收玩家输入就得用input()和类型转换你要统计猜了几次就得用变量累加。这五样东西恰好是 Python 入门阶段最核心的知识点。我见过不少人学 Python 的方式是按章节学今天学循环明天学函数每个知识点单独看都懂但一到综合运用就懵。猜数字游戏的价值就在于它强迫你把这些孤立的知识点拼成一个完整的逻辑链条。你写完之后会发现原来循环里可以嵌判断判断里可以改变量变量又会影响循环的条件——这种知识联动的感觉是单纯刷语法题给不了的。而且它的反馈是即时的。你运行一下输入一个数字程序立刻告诉你大了还是小了这种即时反馈对新手特别重要。学编程最怕的就是写了半天不知道对不对而猜数字游戏让你每改一行代码都能马上看到效果这种正反馈能帮你熬过最开始那段枯燥期。1.2 它足够小小到你敢去改它我特别不建议新手一上来就写什么管理系统、爬虫或者小游戏引擎。那些项目动辄几百行你改一个地方就崩崩了还不知道为什么几次下来信心就没了。猜数字游戏的核心代码也就二三十行你完全有能力把它从头到尾读懂读懂了就敢改。敢改这件事特别关键。比如你想把范围从 1 到 100 改成 1 到 1000只需要改一个数字你想限制最多猜 7 次加一个计数器判断就行你想让它记录玩家猜过的所有数字加个列表就够了。每一次改动都是一次真实的编程练习而且因为项目小改错了也容易回滚。这种低风险试错的环境是新手成长最快的地方。1.3 它天然带着可扩展的接口别看它简单猜数字游戏往上长是有清晰路径的。你可以加难度选择让玩家自己选范围可以加计分系统猜得越少分越高可以加历史记录把每局的成绩存到文件里甚至可以用tkinter给它套一个图形界面。这些扩展每一个都对应着 Python 的一个进阶知识点等于说这个项目本身就是一张学习路线图。我在带新人的时候经常让他们先写基础版然后自己挑一个方向去扩展。有人加了排行榜顺便学会了文件读写有人加了图形界面顺便入了tkinter的门。同一个起点能长出完全不同的技能树这就是猜数字游戏作为教学项目的妙处。2. 动手之前先把这几个基础概念捋清楚2.1 random 模块到底是怎么随机的很多人用random.randint(1, 100)用得很顺手但从来没想过它为什么能生成随机数。这里稍微展开一下因为理解了原理你后面调试的时候会少走很多弯路。计算机本身是确定性的机器它没法凭空产生真正的随机。random模块用的是伪随机数生成器本质上是一个数学公式你给它一个初始值叫种子 seed它就能算出一串看起来毫无规律的数字。默认情况下Python 会用系统时间当种子所以每次运行结果都不一样。但如果你手动设了种子比如random.seed(42)那每次运行生成的数字序列就完全一样——这个特性在调试的时候特别有用后面我会细说。random.randint(a, b)这个函数的作用是生成一个介于 a 和 b 之间包含 a 和 b的整数。注意是包含两端的这跟很多其他语言里的随机函数不一样。比如random.randint(1, 100)有可能返回 1也有可能返回 100。如果你想要不包含右端的效果得用random.randrange(1, 100)它返回的是 1 到 99。提示新手最容易搞混的就是randint和randrange的边界。记住一句话——randint两头都算randrange算头不算尾。2.2 input() 拿到的永远是字符串这是新手踩得最多的一个坑没有之一。input()函数不管你输入的是数字还是字母它返回的永远是字符串类型。也就是说你输入50程序拿到的是50这个字符串而不是数字50。为什么这会导致问题因为字符串和数字比较大小的时候Python 3 会直接报错。你写if guess secret如果guess是字符串50secret是数字50程序会抛出TypeError。所以你必须用int()把字符串转成整数guess int(input(请输入你猜的数字))。但这里还有个隐藏的坑如果玩家输入的不是数字比如输入了abcint(abc)会直接崩溃抛出ValueError。一个健壮的程序应该能处理这种情况而不是让玩家一输错就整个程序挂掉。后面讲完整代码的时候我会给出用try-except处理的办法。2.3 while 循环和 break 的配合逻辑猜数字游戏的主循环通常写成while True然后在猜对的时候用break跳出来。这种写法的好处是循环条件简单退出逻辑集中在判断猜对的那一段读起来清晰。但这里有个细节要注意break只能跳出当前这一层循环。如果你后面嵌套了多层循环break只会跳出最里面那层。猜数字游戏一般只有一层循环所以不用担心这个问题但养成注意 break 作用范围的习惯是好事。另外while True这种写法虽然方便但如果你的退出条件写漏了程序就会变成死循环一直转下去。所以写的时候一定要确保每条可能的路径最终都能到达break或者return。我建议新手在写while True的时候先把退出条件想清楚再动手别写完再补。2.4 变量命名和代码可读性的关系这个点很多人觉得不重要但我必须强调一下。你看下面两段代码# 写法一 a random.randint(1, 100) b 0 while True: c int(input()) b 1 if c a: break# 写法二 secret_number random.randint(1, 100) guess_count 0 while True: guess int(input(请输入你猜的数字)) guess_count 1 if guess secret_number: break两段代码功能完全一样但第二段你一眼就能看懂在干什么第一段得在脑子里翻译一遍。变量名是给未来的自己看的你现在觉得a、b、c省事过两周回来看自己的代码照样一脸懵。所以从一开始就养成用有意义的名字的习惯secret_number、guess_count、max_attempts这种多敲几个字母换来的是长期的可维护性。3. 完整代码逐段拆解与运行验证3.1 基础版完整代码先把能直接跑的完整代码放出来你可以复制到自己的编辑器里保存成guess_number.py然后运行。import random def guess_number_game(): # 生成 1 到 100 之间的随机整数作为答案 secret_number random.randint(1, 100) guess_count 0 max_attempts 7 print( * 40) print(欢迎来到猜数字游戏) print(f我已经想好了一个 1 到 100 之间的整数。) print(f你有 {max_attempts} 次机会猜出它。) print( * 40) while guess_count max_attempts: try: guess int(input(f第 {guess_count 1} 次猜测请输入数字)) except ValueError: print(输入无效请输入一个整数。) continue if guess 1 or guess 100: print(超出范围了请输入 1 到 100 之间的数字。) continue guess_count 1 if guess secret_number: print(太小了再往大猜猜。) elif guess secret_number: print(太大了再往小猜猜。) else: print(f恭喜你猜对了答案就是 {secret_number}。) print(f你一共用了 {guess_count} 次。) return print(f很遗憾{max_attempts} 次机会用完了。) print(f正确答案是 {secret_number}。) if __name__ __main__: guess_number_game()这段代码大概四十行包含了函数封装、异常处理、循环控制、条件判断和格式化输出。下面我一段一段拆开讲。3.2 函数封装为什么要把游戏逻辑包进函数里你可能注意到我把整个游戏逻辑放进了guess_number_game()这个函数里而不是直接写在文件顶层。这么做有几个实际好处。第一方便复用。如果以后你想让玩家连续玩好几局只需要在外部写个循环调用这个函数就行不用改游戏本身的逻辑。第二避免变量污染全局命名空间。如果直接写在顶层secret_number、guess_count这些变量会一直留在内存里项目大了容易跟别的变量撞名。第三方便测试。函数可以单独调用你可以在别的脚本里import它然后测试。最后那个if __name__ __main__:也是同样的道理。它的作用是判断当前文件是不是被直接运行的。如果是直接运行就执行guess_number_game()如果是被别的文件import进去的就不自动执行。这样你的代码既能当脚本跑又能当模块被引用一举两得。3.3 异常处理让程序在玩家乱输入时也不崩try-except这一段是很多新手会忽略的但它恰恰是区分玩具代码和能用的代码的分水岭。try: guess int(input(f第 {guess_count 1} 次猜测请输入数字)) except ValueError: print(输入无效请输入一个整数。) continue这里的逻辑是尝试把玩家输入转成整数如果转不了比如玩家输入了abc或者直接回车就捕获ValueError异常打印提示然后用continue跳过本次循环的剩余部分重新开始下一轮。注意continue的位置很关键。它必须在guess_count 1之前否则玩家输错一次也会被算作一次猜测这显然不合理。我见过有人把计数器加在try里面结果玩家输错也扣次数体验很差。还有一个细节except后面最好指定具体的异常类型ValueError而不是光写一个except:。裸的except会捕获所有异常包括你不想捕获的那些调试的时候会把真正的 bug 藏起来。指定类型是更专业的做法。3.4 范围校验为什么要在判断大小之前先检查范围if guess 1 or guess 100: print(超出范围了请输入 1 到 100 之间的数字。) continue这一段看起来可有可无但其实很有必要。如果玩家输入了 200你不检查范围直接跟答案比程序会告诉你太大了玩家可能会以为 200 这个数字本身是有效的只是比答案大。但实际上 200 根本不在游戏范围内应该明确告诉玩家你输的数不在范围内。而且这个校验放在guess_count 1之前意味着超出范围的输入不消耗次数。这是合理的因为玩家输错范围属于操作失误不应该惩罚。当然你也可以设计成消耗次数这取决于你想让游戏多严格但一定要想清楚再写别让规则变得模糊。3.5 运行验证实际跑一遍看看效果代码写完了得实际跑一下。我用 Python 3.10 测试运行结果大概是这样 欢迎来到猜数字游戏 我已经想好了一个 1 到 100 之间的整数。 你有 7 次机会猜出它。 第 1 次猜测请输入数字50 太小了再往大猜猜。 第 2 次猜测请输入数字75 太大了再往小猜猜。 第 3 次猜测请输入数字62 太小了再往大猜猜。 第 4 次猜测请输入数字68 恭喜你猜对了答案就是 68。 你一共用了 4 次。再测一下异常输入的情况第 1 次猜测请输入数字abc 输入无效请输入一个整数。 第 1 次猜测请输入数字200 超出范围了请输入 1 到 100 之间的数字。 第 1 次猜测请输入数字50 太小了再往大猜猜。可以看到输错两次之后次数还是从第 1 次开始算说明异常处理和范围校验都没有消耗次数逻辑是对的。4. 调试和优化时最容易踩的几个坑4.1 类型不匹配导致的 TypeError这是最高频的错误。如果你忘了写int()直接guess input(...)然后拿guess跟secret_number比大小Python 3 会直接报错TypeError: not supported between instances of str and int这个报错信息其实说得很清楚——字符串和整数之间不支持比较。看到这个错误第一反应就是检查是不是漏了类型转换。Python 2 时代字符串和数字比较不会报错会按 ASCII 码比结果更隐蔽所以从 Python 2 转过来的人特别容易栽在这上面。4.2 死循环退出条件写漏了死循环的典型症状是程序一直让你输入永远不结束。常见原因有几个一是break写在了错误的缩进层级比如写在了if外面导致永远执行不到二是循环条件写反了比如while guess_count max_attempts三是guess_count忘了自增导致条件永远满足。排查死循环有个笨办法但很有效在循环体第一行加一句print(fdebug: guess_count{guess_count})运行一下看这个值有没有变化。如果一直不变那就是自增语句没执行到检查缩进和位置。4.3 随机数不可复现导致调试困难调试的时候最烦的就是每次运行答案都不一样你没法稳定复现某个 bug。解决办法是在生成随机数之前固定种子random.seed(42) secret_number random.randint(1, 100)这样每次运行答案都是同一个种子 42 对应的答案我测下来是 82你就能稳定地测试各种输入路径。等调试完了把random.seed(42)这行删掉或者注释掉游戏就恢复正常随机了。注意random.seed()只需要在程序开始时调用一次不要放在循环里否则每次循环都会重置随机序列反而出问题。4.4 输入带空格或换行导致的意外input()会自动去掉末尾的换行符但不会去掉首尾空格。如果玩家输入了 50 int( 50 )其实是可以正常转换的Python 会自动忽略首尾空白。但如果玩家输入的是5 0中间有空格int()就会报ValueError。这种情况比较少见但如果你想让程序更健壮可以在转换前先strip()一下或者干脆用正则校验。4.5 用二分法思维优化猜测策略这个不算 bug但值得提一下。1 到 100 的范围最优策略是二分法最多 7 次一定能猜中因为 2 的 7 次方是 128大于 100。所以我把max_attempts设成 7是经过计算的不是随便定的。如果你把范围改成 1 到 1000那最优次数是 10 次2 的 10 次方是 1024。这个规律可以帮你合理设置难度max_attempts math.ceil(math.log2(范围大小))。当然给玩家留点余量会更友好比如范围 100 给 8 次范围 1000 给 12 次。5. 把这个小游戏改造成更完整的项目5.1 加一个难度选择菜单基础版玩几局就腻了加个难度选择能明显提升可玩性。思路是在游戏开始前让玩家选范围然后根据选择设置不同的max_attempts。def choose_difficulty(): print(请选择难度) print(1. 简单1-5010 次机会) print(2. 普通1-1007 次机会) print(3. 困难1-5009 次机会) while True: choice input(输入 1、2 或 3) if choice 1: return 50, 10 elif choice 2: return 100, 7 elif choice 3: return 500, 9 else: print(无效选择请重新输入。)这个函数返回一个元组(上限, 最大次数)主函数拿到之后解包使用。注意困难模式范围是 500最优次数是 92 的 9 次方是 512所以给 9 次是合理的。5.2 用文件保存历史成绩想让游戏有成长感可以把每局的成绩存下来。最简单的做法是追加写入一个文本文件def save_score(player_name, guess_count, secret_number): with open(scores.txt, a, encodingutf-8) as f: f.write(f{player_name},{guess_count},{secret_number}\n)这里用a模式是追加不会覆盖之前的记录。encodingutf-8是为了避免中文名字乱码。读取的时候用r模式逐行split(,)解析就行。这个练习能让你顺便掌握文件读写性价比很高。5.3 用 tkinter 套一个图形界面如果你已经学完了基础语法想往 GUI 方向走tkinter是最好的起点因为它是 Python 自带的不用额外安装。核心思路是把input()换成Entry输入框把print()换成Label或者Text显示把循环换成按钮的回调函数。import tkinter as tk from tkinter import messagebox import random class GuessNumberGUI: def __init__(self, root): self.root root self.root.title(猜数字游戏) self.secret random.randint(1, 100) self.count 0 tk.Label(root, text请输入 1-100 之间的数字).pack() self.entry tk.Entry(root) self.entry.pack() tk.Button(root, text猜一猜, commandself.check).pack() self.result tk.Label(root, text) self.result.pack() def check(self): try: guess int(self.entry.get()) except ValueError: self.result.config(text请输入整数) return self.count 1 if guess self.secret: self.result.config(text太小了) elif guess self.secret: self.result.config(text太大了) else: messagebox.showinfo(恭喜, f猜对了用了 {self.count} 次) self.root.destroy() if __name__ __main__: root tk.Tk() GuessNumberGUI(root) root.mainloop()这段代码把游戏逻辑封装成了一个类用mainloop()启动事件循环。注意 GUI 版本里没有while循环了因为界面本身就是靠事件驱动的——每次点按钮触发一次check这跟命令行版的思路完全不同值得体会一下。5.4 加一个猜过的数字记录功能这个功能能帮玩家做排除法提升策略性。用一个列表存已经猜过的数字每次猜之前检查一下是否重复guessed_numbers [] # 在循环里 if guess in guessed_numbers: print(这个数字你已经猜过了换一个吧。) continue guessed_numbers.append(guess)注意这个检查要放在guess_count 1之前重复猜测不应该消耗次数。另外列表的in操作是线性查找数字多了会慢但猜数字游戏最多几十次完全不用担心性能。6. 从这个小项目里能带走的通用编程习惯6.1 先想清楚退出条件再写循环写任何循环之前先问自己三个问题循环什么时候开始什么时候结束结束之后要做什么猜数字游戏里开始是玩家第一次输入结束是猜对或者次数用完结束后要显示答案。把这三个问题想清楚循环就不会写成死循环。这个习惯放大到任何项目都适用。我见过太多人写循环写到一半发现退不出来回头改结构结果越改越乱。根子就在于动手之前没想清楚退出条件。6.2 把用户可能乱来当成默认假设新手写代码容易假设用户会乖乖按提示输入但现实是用户什么都可能输。空字符串、字母、超长数字、特殊符号甚至直接关掉窗口。你的程序要么优雅处理要么至少别崩溃。try-except是最基本的防线范围校验是第二道输入去空格是第三道。这三道防线加起来程序就能应对绝大多数异常输入了。这个思路在写任何跟用户交互的程序时都用得上。6.3 用函数把逻辑切块基础版四十行代码如果全堆在顶层读起来还行。但如果你加了难度选择、历史记录、排行榜代码很快就上百行了。这时候如果不拆函数维护起来就是灾难。拆函数的原则是一个函数只做一件事。choose_difficulty只负责选难度save_score只负责存成绩guess_number_game只负责游戏主流程。每个函数职责单一测试和修改都方便。这个习惯越早养成越好等到项目大了再改就痛苦了。6.4 调试信息要能随时开关调试的时候加print很方便但调试完忘了删正式运行时就会输出一堆乱七八糟的东西。更好的做法是用logging模块或者定义一个DEBUG开关DEBUG False def debug_print(*args): if DEBUG: print([DEBUG], *args)这样调试的时候把DEBUG设成True发布的时候设成False一行切换干净利落。这个技巧在稍微大一点的项目里几乎是标配。6.5 代码写完要自己当一次刁钻用户最后一步也是最容易被跳过的一步把自己当成最不配合的用户故意输错、故意乱按、故意在边界值上试探。输入 1、输入 100、输入 0、输入 101、输入负数、输入空、输入字母每一种都试一遍看看程序反应是否合理。我每次写完交互类代码都会做这个自虐测试十次有八次能发现之前没想到的边界情况。比如有一次我发现玩家输入-5时程序提示太小了但其实-5根本不在范围内应该提示超出范围。这种问题只有自己当刁钻用户才能发现。猜数字游戏这个项目代码量不大但能练到的东西一点不少。从最基础的变量、循环、判断到异常处理、函数封装、文件读写再到 GUI 和调试技巧它像一颗种子你想让它长多大都行。我建议你先照着基础版敲一遍确保能跑起来然后挑一个扩展方向自己动手改。改的过程中遇到问题再回头查对应的知识点这种以项目驱动学习的方式比按部就班看教程效率高得多。等你把这个小游戏玩透了再去看那些几百行的项目心里就有底了。