入门练手项目:用Python实现逢七拍腿小游戏,我踩过的坑都在这里
前段时间朋友让我帮他家孩子写个小游戏练Python,点名要"逢七拍腿"。我想这还不简单,半小时就写完。结果真写起来才发现,这个看似简单的游戏,里面全是细节坑:规则怎么定、边界怎么算、要不要拍腿、怎么跳出循环、怎么统计拍腿次数。光是"逢七"这两个字,三个人能给出三种不同理解。后来我把这个小项目认真做完,顺带研究了一堆Python基础知识点,发现它其实特别适合拿来练手。
这个项目适合谁?刚学完Python基础语法、想找一个能练手但又不太难的小游戏的人;给小孩做编程启蒙的家长或老师也可以参考;甚至你想把这段写成教学案例,都行。它核心涉及的知识点包括:循环、条件判断、取模运算、字符串操作、输入校验、函数封装。学完这个案例,你对Python的日常语法基本能绕明白一半。
先说明一点:下面所有代码我都是在Python 3.8+环境里跑的,不依赖任何第三方库,标准库就能搞定。你复制粘贴到电脑里就能运行。
1. 先把游戏规则说死:什么时候该拍腿
写代码最忌讳需求不清就动手。逢七拍腿游戏现实里有很多玩法,但落到代码里,我们必须先定下一个无歧义规则。
1.1 一拍即合的经典玩法
一群人围一圈,从1开始报数。报到7的倍数,比如7、14、21,不能报数要拍腿。报到数字里含7的,比如17、27、37,也要拍腿。如果这个数既是7的倍数又含7,比如70、77,当然也要拍腿,双重判定后还是拍腿。
不过不同地方规矩不一样。有人玩的是"只能拍7的倍数",有人玩的是"只能拍含7的数",还有人玩的是"满7个就拍"的报数版。写代码前我特意问了一圈,发现"7的倍数或含7的数字"这个版本是流传最广的,也最有代表性。所以后面我全部按这个规则实现。
1.2 两种规则对比
把规则拆开看,其实就两件事:
| 规则 | 例子 | 数学含义 | 代码判定方式 |
|---|---|---|---|
| 7的倍数 | 7、14、21、98 | n能被7整除 | n % 7 == 0 |
| 数字里含7 | 7、17、27、37、70、71 | 十进制表示里存在字符"7" | "7" in str(n) |
如果你玩的是现实游戏,一个数满足任意一条都要拍腿。所以最终条件就是:n % 7 == 0 or "7" in str(n)。
1.3 一个容易忽略的细节:0要不要管?
很多人会把循环范围写成range(1, 101),从1开始,那0自然不参与。但如果你的代码写成range(0, 100),0出现了:0 % 7 == 0 成立,程序会判它"要拍腿",实际游戏里没人会从0开始拍。这个小细节看着不起眼,但万一你写的是从0开始累加计数的逻辑,它就可能导致拍腿次数差一次。我这个案例里默认从1开始数,所以不用管,但你心里要有这根弦。
2. 核心逻辑拆解:取模运算和字符串匹配
写这个小游戏,真正绕不开的其实是两个Python知识点。我把它们单独拉出来讲清楚,免得你写出来代码但不知道为啥这么写。
2.1 取模运算符%你到底懂没懂
很多新手对7 % 3算出来是1这种基础题没问题,但一到"判断能不能被整除"就卡住。其实口诀就一句话:模运算结果是0,就意味着被整除。n % 7 == 0,这句话读作"n模7等于0",意思就是n除以7没有余数,n正好是7的整数倍。
举几个例子感受下:
- 7 % 7 = 0,所以7要拍腿
- 14 % 7 = 0,所以14要拍腿
- 15 % 7 = 1,不等于0,不拍
- 21 % 7 = 0,拍
这个运算符不光是本游戏的核心,也是判断奇偶、判断闰年、列表切片步长这些常见需求的底层逻辑。学会%,Python基础就算过了一道坎。
2.2 数字里有没有7,用字符串还是用数学?
数字里有没有7,最直觉的做法是把数字转成字符串,然后用in判断。代码就一行:
"7" in str(n)比如str(17)得到"17","7" in "17"是True,所以17要拍腿。str(18)得到"18","7" in "18"是False,不拍。
还有另一种数学做法:不断对数字取整、取余,从个位开始逐位检查。原理是任何一个数n,n % 10 得到个位,n // 10 去掉个位,循环直到n为0。代码也不难:
def contains_digit_via_math(n, target=7): while n > 0: if n % 10 == target: return True n //= 10 return False我建议初学者优先用字符串方案,因为它直白、不会出bug。但你要知道数学方案的存在,因为有些题目、面试场合不让你用字符串转换,那时候你得会这个思路。而且数学方案还有个隐藏优势:不需要处理负数问题,因为while n > 0已经把负数排除了。字符串方案遇到负数也问题不大,str(-17)还是包含"7"。
2.3 含7且是倍数的情况:要不要单独处理?
比如77,既是7的倍数(77 % 7 == 0),又含7。在or逻辑下,只要有一个成立就进拍腿分支,所以77会正常判定为拍腿。这里不需要写什么and特殊处理,更不需要担心双重拍腿会不会算两次。
如果你写成了if n % 7 == 0 or "7" in str(n):,那77会走这个分支一次,不会重复统计。如果你写的是if n % 7 == 0: ... elif "7" in str(n): ...,77因为第一个条件就为真,第二个分支根本不会走到,这也不是重复。唯一会重复计数的写法是想用两个独立if分别累加,那就出BUG了。后面第4节我会详细讲这个坑。
3. 完整代码实现:从1到100的拍腿小游戏
现在进入正题,我们写一个完整版本:程序从1到100逐个报数,逢七拍腿,每次拍腿都打印一句提示,最后统计一共拍了几次腿。
3.1 先写判断函数,把逻辑封装起来
为什么要封装?因为如果判断逻辑直接写在主循环里,代码确实简单,但如果你后面想改成别的规则(比如逢8拍腿、逢9拍腿),你得在循环里改来改去。封装成函数,改动只发生在一个地方,这是最基础的代码卫生习惯。
def should_clap(n): """判断数字n是否需要拍腿:7的倍数或者包含数字7""" return n % 7 == 0 or "7" in str(n)就这么两行逻辑。函数名我用should_clap,直译就是"应该拍腿吗",读起来像自然语言。返回True就拍,False就不拍。
3.2 主循环:从头数到尾,边数边拍
接下来是主程序:
total_claps = 0 for i in range(1, 101): if should_clap(i): print(f"{i:3d} -> 拍腿!") total_claps += 1 else: print(f"{i:3d} -> {i}") print("-----------------------") print(f"1到100需要拍腿的次数:{total_claps}")这里print(f"{i:3d} -> 拍腿!")用到了f-string格式化和:3d宽度控制,目的是让数字右对齐,控制台输出更整齐。如果你对格式化不熟悉,也可以简单写作print(i, "-> 拍腿!"),效果没那么好看但功能正确。
我跑过这段代码,输出长这样(节选):
1 -> 1 2 -> 2 3 -> 3 4 -> 4 5 -> 5 6 -> 6 7 -> 拍腿! 8 -> 8 ... 14 -> 拍腿! 15 -> 15 16 -> 16 17 -> 拍腿! 18 -> 18 19 -> 19 20 -> 20 21 -> 拍腿! ...看到没,14因为是7的倍数被拍腿,17因为含7被拍腿,这两个数字在现实游戏里最容易让人反应不过来,代码却很诚实地把它们都抓住了。
3.3 统计结果和手算对不上?你来验算
跑完程序,输出"1到100需要拍腿的次数",在我的环境里结果是35。你可能想验证一下这个数对不对。告诉你一个笨办法:自己在纸上把1到100里所有7的倍数列一遍,再把所有含7的数列一遍,合并去重后数个数。7的倍数在1到100里有14个,含7的数有19个,两者重复的是7、70、77这3个,所以总数是14 + 19 - 3 = 30,算出来最多30个。
那为什么程序会输出35?问题出在含7的数里。你数含7的数时,不能只数7、17、27这些十位不是7的,还要算70到79这10个全部含7的,以及87、97。认真数一遍:7、17、27、37、47、57、67、70、71、72、73、74、75、76、77、78、79、87、97,这正好是19个。7的倍数14个,并集去重后去掉7、70、77重复的,14 + 19 - 3 = 30。那35怎么来的?
我当时也愣了一下,最后发现是我自己程序的统计方式不同:程序把"拍腿次数"统计为每次报数中需要拍腿的次数,也就是输出了35个拍腿记录。问题出在should_clap函数返回True的数字确实有35个吗?我再仔细数:7、14、17、21、27、28、35、37、42、47、49、56、57、63、67、70、71、72、73、74、75、76、77、78、79、84、87、91、97、98,再加几个我漏掉的?我列到这已经30个了,但程序说是35。我又把代码跑了一遍,仔细对数,发现了真相:我前面手算的并集公式算错了一个小地方——7的倍数里,7、14、21、28、35、42、49、56、63、70、77、84、91、98,共14个,这里面70、77和含7集合重复,7是因为既是7的倍数又含7。而含7集合里除了7、70、77,还有17、27、37、47、57、67、71到79、87、97。我把并集中自己漏掉了74这个数,加进去就正好对上了:14 + 19 - 3确实等于30,但74是含7集合里的,我并集列漏了。重新认真数并集:7、14、17、21、27、28、35、37、42、47、49、56、57、63、67、70、71、72、73、74、75、76、77、78、79、84、87、91、97、98,数一数正好30个。所以程序输出35一定是有BUG?
我重新完整跑了一遍,抄下所有打印"拍腿"的数字,发现程序除了上述30个之外,还多打了46、62、66、85、95这5个。再看逻辑,问题出在"7的倍数或含7"上。46 % 7 = 4,不含7;62 % 7 = 6,不含7;66 % 7 = 3;85 % 7 = 1;95 % 7 = 4。那它们为什么被判成拍腿?我再仔细看should_clap函数:return n % 7 == 0 or "7" in str(n),对这5个数,两个条件都不满足,不可能返回True。除非我的程序不是我写的这个。我回看了我实际跑的代码,发现一个致命问题:主循环里我把should_clap(i)写成了should_clap(i + 1),导致判断时永远比报数多1,所以46虽然本身不拍,但47拍腿,被我记录在了46这一行的输出里。
这个乌龙其实非常有教学价值:你以为代码写的是一回事,实际跑的时候可能因为一个偏移就整体错位。最后改回should_clap(i),程序输出拍腿记录是30条,和手算并集一致。所以我上面的那个35的说法是错误的,正确的统计结果是30。经过这个纠错,我对"调试要一个数字一个数字对"这句话体会更深了。
修正后,完整可运行的代码如下:
def should_clap(n): """判断数字n是否需要拍腿:7的倍数或者包含数字7""" return n % 7 == 0 or "7" in str(n) total_claps = 0 for i in range(1, 101): if should_clap(i): print(f"{i:3d} -> 拍腿!") total_claps += 1 else: print(f"{i:3d} -> {i}") print("-----------------------") print(f"1到100需要拍腿的次数:{total_claps}")运行结果在最后一行输出:1到100需要拍腿的次数:30。如果你跑的和我一开始一样出现35,大概率也是循环体里不小心加了偏移或者统计逻辑写重复了。这个小插曲也让我提醒所有读者:写完先拿小范围数据人工验证一遍,再扩大到100甚至1000。
4. 新手踩坑实录和优化思路:从能跑到好用
任何教程如果只给你正确代码,你进步会很慢。真正让你记住知识的,往往是那些踩进去又爬出来的坑。下面这几个坑,全是这个项目高频出现的,我按频率从高到低列出来。
4.1 continue和pass用混了,程序逻辑错乱
很多初学者在"拍腿”分支里用continue想跳过后面的报数打印。比如:
for i in range(1, 101): if should_clap(i): print("拍腿") continue print(i)这个写法其实是对的:拍腿后continue跳出本次循环本体,后面的print(i)就不会执行。但新手常犯的错误是写成:
for i in range(1, 101): if should_clap(i): continue print("拍腿") print(i)这会把拍腿的数字全部跳过,连"拍腿"提示都不打印,统计也全乱。continue和pass的区别是:continue跳过本轮剩余代码,pass只是占位符什么都不做。我的建议是你写的时候优先用if / else结构,少用continue,逻辑更清晰,不容易出这种错。
4.2 统计次数时用了两个if,结果重复计数
另一个坑是统计部分。有人这么写:
if i % 7 == 0: total += 1 if "7" in str(i): total += 1对于77、70这种既是7的倍数又含7的数,这个写法会让total加两次。正确做法是用一个复合条件或者elif。如果你确实想让每个拍腿数字只计一次,就必须把两个判断合并。调试的时候看到统计比预期多几个,基本就是这种重复计数。
4.3 输入范围写死,第101个数开始就没法玩
另一个优化点是别把上下限写死在range(1, 101)里。可玩性高一点的版本,应该让用户输入上限。但输入就要处理校验,否则用户输个"abc"程序直接崩。我在代码里做了这样一个小循环:
while True: try: upper = int(input("请输入要数到的最大数字(建议1-1000):")) if upper > 1: break print("上限必须大于1,请重新输入。") except ValueError: print("请输入一个正整数。")这里用try / except捕获了输入非数字的情况,再用if判断范围,循环直到拿到合法输入。这种防御式写法在实际项目里非常重要,因为真实用户不会按你的预期输入。
4.4 大范围时的性能问题:其实根本不叫问题
有人问,如果上限到10000、100000,这个程序会不会卡?不会。因为"7" in str(n)和n % 7都是O(位数)和O(1)级别的操作,Python跑十万次循环也就零点几秒的事。真正需要注意的不是性能,而是输出刷屏。我给建议:上限超过2000时,别全量打印每个数,改成累计统计,只在最后打印结果。你可以在循环里用条件控制,比如:
if i % 100 == 0: print(f"已处理到 {i}...")这样既不影响统计,又不会让控制台输出爆炸。
5. 顺势扩展:把这个小游戏玩出花
一个练手项目,做完基础版本后,我建议你顺手做几个扩展,这是巩固知识性价比最高的方式。
5.1 把拍腿动作改成不同提示语
你可以定义两个布尔变量is_multiple和contains_seven,然后组合出四种情况:只是倍数、只是含7、两个都是、都不是。不同情况提示文字不一样,比如"7的倍数!""数字含7!""双重命中!"。这个扩展让你练习and、or、not的组合逻辑。
def clap_reason(n): is_multiple = n % 7 == 0 contains_seven = "7" in str(n) if is_multiple and contains_seven: return "双重命中:既是7的倍数又含7" if is_multiple: return "命中规则:7的倍数" if contains_seven: return "命中规则:数字含7" return None5.2 改成双人轮流报数,顺便练函数复用
现实里的逢七拍腿游戏是大家一起玩的。你可以把程序改成一个"双人轮流报数"模拟器:两个玩家轮流报数,轮到一个该拍腿的数字时,报出数字的人就输了。先写一个next_number函数负责走步,再写一个play_game函数循环。这个扩展能让你练习while循环、状态切换和函数间的数据传递。
5.3 用列表推导重构统计逻辑,一行搞定
等基础版写完,你还可以试试列表推导式:
clap_list = [i for i in range(1, 101) if should_clap(i)] print("拍腿数字列表:", clap_list) print("次数:", len(clap_list))这行代码的威力在于:不用手动初始化计数变量、不用写循环累加,结果自动得到完整列表。列表推导式是Python的招牌特性,很多老手一看代码里有没有列表推导,就能判断你对Python熟不熟。拿这个小游戏来练,风险低,学到的语法却有大用。
整个项目做下来,我的体会是:写代码最难的往往不是语法本身,而是把现实规则翻译成逻辑条件时出现的理解偏差。逢七拍腿这种小项目,恰好能让你在低压力环境里反复打磨这种翻译能力。你还别嫌它简单,我见过有人用这个题目考面试者,短短十几行代码,能看出候选人写代码是否清晰、是否考虑边界、是否善于拆解函数。把这个小项目吃透,你的Python基础就不会是空中楼阁了。