☰
Python新手练手项目:从零实现猜数字游戏,掌握random与input核心用法
2026/9/25 1:34:43 网站建设 项目流程

很多刚接触 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是字符串"50",secret是数字50,程序会抛出TypeError。所以你必须用int()把字符串转成整数:guess = int(input("请输入你猜的数字:"))。

但这里还有个隐藏的坑:如果玩家输入的不是数字,比如输入了"abc",int("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(f"debug: 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-50,10 次机会)") print("2. 普通(1-100,7 次机会)") print("3. 困难(1-500,9 次机会)") 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,最优次数是 9(2 的 9 次方是 512),所以给 9 次是合理的。

5.2 用文件保存历史成绩

想让游戏有"成长感",可以把每局的成绩存下来。最简单的做法是追加写入一个文本文件:

def save_score(player_name, guess_count, secret_number): with open("scores.txt", "a", encoding="utf-8") as f: f.write(f"{player_name},{guess_count},{secret_number}\n")

这里用"a"模式是追加,不会覆盖之前的记录。encoding="utf-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="猜一猜", command=self.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 和调试技巧,它像一颗种子,你想让它长多大都行。我建议你先照着基础版敲一遍,确保能跑起来,然后挑一个扩展方向自己动手改。改的过程中遇到问题,再回头查对应的知识点,这种"以项目驱动学习"的方式,比按部就班看教程效率高得多。等你把这个小游戏玩透了,再去看那些几百行的项目,心里就有底了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询