刚学 Python 的第一份实验清单,基本逃不过这五道题:三位数组合、素数判断、四叶玫瑰数、字符统计、九九乘法表。我第一次写的时候,前两道题就把自己卡到怀疑人生,后来回头一看,问题不在语法不会,而是“循环到底套几层、边界到底取到哪”这两件事没想明白。这篇博客把我跑通这套题的完整过程整理出来了,从每道题的思路拆解,到可以直接抄的代码,再到那些老师批改时容易挑刺的格式细节,全部放在一起。准备做实验的新手可以按顺序跟写,期末复习的人可以直接跳过前面看第七节的避坑名单。
1. 先看清楚五道题在考什么,再动手敲代码
1.1 一张清单,五类必练能力
这套实验的价值不完全在于“写出答案”,而在于每道题都在训练一种具体的能力。我先把五个题目的表层考点和真实训练点拆开列一张表,后面每一题再展开讲。
| 题号 | 题目 | 表面考点 | 真正训练的能力 |
|---|---|---|---|
| 1 | 三位数组合 | 三重循环、条件判断 | 枚举与剪枝意识,先算清楚再写代码 |
| 2 | 素数判断 | 循环、取余 | 算法优化思路,减少无效运算 |
| 3 | 四叶玫瑰数 | 取位、幂运算 | 把数学问题翻译成循环和条件 |
| 4 | 统计数 | 循环、分支、累加 | 输入处理与分类统计的边界判断 |
| 5 | 九九乘法表 | 嵌套循环、格式化输出 | 循环层数和输出格式的控制 |
这张表看完你会发现,五道题用到的语法其实高度重复,就是循环加条件,再加一点字符串或格式化输出。区别在于每一题对循环层数、边界判断、输出格式的要求不同。所以做题顺序可以不用跟着题号走,先把规律最明显的九九乘法表写了,再去做三位数组合,你对嵌套循环的理解会顺很多。
1.2 我的实验环境和文件组织
现在装 Python 很简单,官网下个安装包,装的时候把 Add Python to PATH 勾上,之后在终端敲 python --version 能出版本号就说明装好了。写代码我习惯用 VS Code,补全和调试比自带 IDLE 舒服,装了 Python 插件之后直接 F5 就能跑。这套实验完全不需要第三方库,只用标准库,所以环境问题基本不花时间。
一个小提醒:文件名尽量用英文字母,比如 exp1.py,别用中文文件名。Windows 下中文文件名在部分终端的编码处理上不省心,万一报错你容易误判成代码问题,实际是文件路径编码问题。这个坑不在题目里,但很真实。
2. 第一题:三位数互不相同且无重复数字,先心算再写循环
2.1 先想清楚再编码:百位4种,十位3种,个位2种
题目的标准问法一般是:用 1、2、3、4 这四个数字,能组成多少个互不相同且无重复数字的三位数?并把这些三位数全部输出。
先别急着敲键盘,用排列数心算一下:百位可以从四个数字里任选一个,4种;十位不能和百位重复,剩3种;个位再从剩下的两个数字里选,2种。所以总数是 4×3×2=24 个。心算这一步特别重要,不是为了直接交答案,而是给自己一个预期输出规模。
很多同学上来就写循环,结果输出到底是 24 个还是 64 个都没概念,程序跑完对不对,完全没法判断。做实验题,先有预期再动手,这本身就是一种调试意识训练。
2.2 三重循环加条件判断,最稳的写法
直接上代码:
count = 0 nums = [] for i in range(1, 5): for j in range(1, 5): for k in range(1, 5): if i != j and j != k and i != k: num = i * 100 + j * 10 + k nums.append(num) count += 1 print("一共可以组成", count, "个三位数") print(nums)这里的 i、j、k 分别代表百位、十位、个位的数字。range(1, 5) 产生的是 1、2、3、4,取不到 5,刚好符合我们只从这四个数字里选的要求。三重循环会枚举出所有 64 种排列,包括 111、112 这类带重复数字的组合,if 的三个条件把它们拦住。
三个条件 i != j、j != k、i != k 一个都不能少。只写两个就会漏判,比如只判断 i != j 和 j != k,那 121 这种百位和个位重复的数就会混进结果。把结果收集到列表再统一打印,比循环里一个一个 print 好,因为实验报告通常要的是“结果加数量”,列表能让你一眼检查有没有漏项或重复项。
2.3 变式:如果题目改成100到999之间呢
有些版本会把题目改成:求 100 到 999 之间所有各位数字互不相同的三位数,并统计个数。这种版本只是枚举范围变了,百位不能为 0,所以外层 i 从 1 到 9,内层 j 和 k 可以用 0 到 9,判断条件完全一样。
count = 0 for i in range(1, 10): for j in range(0, 10): for k in range(0, 10): if i != j and i != k and j != k: print(i * 100 + j * 10 + k) count += 1 print("一共", count, "个")如果不想用三重循环,Python 的 itertools.permutations 可以一行完成:
from itertools import permutations for a, b, c in permutations(range(1, 5), 3): print(a * 100 + b * 10 + c)一般实验课不要求写成这样,我提它只是让你知道还有这条路可选,核心还是把三重循环练明白。
3. 第二题:判断并输出素数,别只会写“有因数就排除”
3.1 初版试除法,先把功能跑通
素数的定义很直接:大于 1 的自然数,除了 1 和它本身以外没有其他因数。所以最朴素的做法就是试除,从 2 一直试到这个数减 1,只要发现能整除,说明不是素数。
def is_prime(n): if n < 2: return False for i in range(2, n): if n % i == 0: return False return True这个版本不算错,运行结果也对。但没必要把 i 试到 n-1,判断一个数 n 是不是素数,试到 n-1 是纯粹的算力浪费。n 一旦大起来,循环次数就上去了。实验课可能看不出差别,但这道题最值得学的就是后续的优化思路。
3.2 拆掉重复劳动:平方根边界和偶数跳过
数学上有个结论:如果 n 有一个大于 sqrt(n) 的因数,就一定有一个小于 sqrt(n) 的因数跟它配对。所以只要在 2 到 sqrt(n) 范围内没有因数,n 就是素数。循环上界可以改成 int(n ** 0.5) + 1。
另一个优化:除了 2,所有偶数都不可能是素数。所以判完 n == 2 之后,可以直接把 n % 2 == 0 的情况过滤掉,然后让循环从 3 开始,每次加 2,只看奇数。试除次数又少一半。
def is_prime(n): if n < 2: return False if n == 2: return True if n % 2 == 0: return False m = int(n ** 0.5) for i in range(3, m + 1, 2): if n % i == 0: return False return True这里有个很细节的坑:m 取整之后要不要加 1?int(n ** 0.5) 对完全平方数得到的就是平方根,比如 n=25,int(5.0)=5,循环 range(3, 6, 2) 会试到 5,25 能被 5 整除,能正确返回 False。所以写成 range(3, m + 1, 2) 就包含了 m 本身,不会漏掉平方根这个边界。有人写成 range(3, m),那判断 25 时就漏了 5,25 会被误判成素数。
3.3 把指定范围内的素数一次性输出
判断函数写好之后,输出就简单了。很多实训平台上这道题被拆成“输出指定范围内的素数”,本质就是先把判断函数做对,再用 range 遍历筛选。
primes = [] for x in range(2, 101): if is_prime(x): primes.append(x) print(primes)写多了之后可以压缩成列表推导式:
primes = [x for x in range(2, 101) if is_prime(x)] print(primes)如果题目要求“输出 100 以内的素数”而不是返回列表,就把 append 换成 print(x),在循环里直接输出。两种写法老师都认,关键是别漏掉 0 和 1 的前置判断,这两个数不是素数,必须返回 False。很多人写完 is_prime 直接套 range,结果把 1 也列进素数表里,这种错误很低级但考场里真的会犯。
4. 第三题:四叶玫瑰数,拆位数这件事比背结果更重要
4.1 水仙花数的家族谱
四叶玫瑰数第一次听会觉得很玄,其实它属于自恋数家族:一个 n 位数,如果它的每一位数字的 n 次方之和等于它本身,这个数就叫自恋数。三位数的自恋数叫水仙花数,四位数的就是四叶玫瑰数,再往后还有五角星数、六合数、北斗七星数、八仙数、九九重阳数、十全十美数。
三位水仙花数只有 153、370、371、407 四个,四叶玫瑰数只有 1634、8208、9474 三个。这三个数字建议背一下,因为代码写完跑出来是不是这三个数,自己马上就能验证。如果多出来一个或少一个,先别怀疑题目,回去看取位和乘方那两行。
4.2 两种取数位写法,一种更直观一种更简洁
固定四位数的取位,最直观的是除法和取余:
for n in range(1000, 10000): a = n // 1000 b = n // 100 % 10 c = n // 10 % 10 d = n % 10 if a ** 4 + b ** 4 + c ** 4 + d ** 4 == n: print(n)四行取位的逻辑:千位直接整除 1000;百位先整除 100 再取余 10;十位先整除 10 再取余 10;个位直接取余 10。顺序别算错。我见过同学把百位写成 n % 100 // 10,结果也对,因为取模 100 得到末两位,再整除 10 得到十位。思路等价,但建议挑一套自己最不会混的统一写法。
字符串解法更简洁:
for n in range(1000, 10000): s = str(n) total = 0 for ch in s: total += int(ch) ** 4 if total == n: print(n)字符串取位有个天然优势:数字长度变化时,s 的长度会自动匹配,不需要改取位逻辑。缺点是速度比整数运算慢一些,但这套实验里 n 最多一万个,完全没感觉。
4.3 把三位改成任意位数
想做得更有通用性,可以把位数抽象成参数:
def narcissistic(digits): for num in range(10 ** (digits - 1), 10 ** digits): s = str(num) if sum(int(d) ** digits for d in s) == num: print(num) narcissistic(3) # 三位水仙花数 narcissistic(4) # 四位四叶玫瑰数注意传给 range 的范围:digits=3 时从 100 到 999,符合三位数定义。如果想找一位数需要单独处理成 range(0, 10),因为一位数可以包含 0,不过实验题基本不涉及。
最后提醒一个巨大的坑:Python 里 ** 才是乘方,^ 是按位异或。写 a^4 的话,程序不报错,但结果必然不对。a=1 时 a^4 等于 1 异或 4,不是 1 的四次方。这个隐蔽错我当年排查了很久才发现,先说给你听,省得你踩同样的坑。
5. 第四题:“统计数”这道题,我见过两种出法
5.1 版本A:统计一行字符串里的字母、数字、空格和其他字符
我见过最多的是这个版本:输入一行字符,统计其中英文字母、数字、空格和其他字符的个数。核心是四个累加变量加一层循环,判断方式决定代码稳不稳。
s = input("请输入一行字符:") letters = digits = spaces = others = 0 for ch in s: if ch.isalpha(): letters += 1 elif ch.isdigit(): digits += 1 elif ch.isspace(): spaces += 1 else: others += 1 print(f"英文字母:{letters} 个") print(f"数字:{digits} 个") print(f"空格:{spaces} 个") print(f"其他字符:{others} 个")这里有个容易忽视的细节:isalpha() 对中文也返回 True。如果题目明确要求统计英文字母,而输入里夹杂了汉字,这个写法会把中文字符也算进 letters,结果不符合题目要求。稳妥的做法是用 ASCII 区间判断:
if 'a' <= ch <= 'z' or 'A' <= ch <= 'Z': letters += 1Python 里字符串之间可以直接比较大小,因为比较的是 Unicode 码点,所以这个写法成立,而且只认英文字母,不误伤中文。
5.2 版本B:统计范围内的奇偶数或某种特定数
另一个常见版本是统计 1 到 100 之间奇数和偶数的个数,或者统计某段范围内能被 3 整除的数的个数。这种题不需要字符串判断,直接循环取余计数。
odd = even = 0 for i in range(1, 101): if i % 2 == 0: even += 1 else: odd += 1 print(f"奇数:{odd} 个,偶数:{even} 个")如果你拿到的实验题目只写了“统计数”三个字,上下文又不清晰,最好的判断办法是看上一题和下一题。这套实验里统计题通常是嵌在字符串处理或循环控制那一组的,跟着上下文判断是字符统计还是整数统计,方向基本不会错。
5.3 统计题最容易翻车的三个细节
第一个坑:input() 永远返回字符串。实验要求统计整数输入时,如果忘了 int() 转换,后面所有运算都会变成字符串拼接,结果错得很离谱。
第二个坑:字符串判断方法对特殊字符的判定很宽松。比如 isdigit() 对“①”这类带圈数字在某些 Python 版本下也会返回 True。做实验时输入纯 ASCII 不会遇到,但养成用区间判断的习惯更稳,因为行为完全可控。
第三个坑:统计结果要自己先估算。统计 1 到 100 的奇偶数,奇数和偶数应该各 50,这是心算能验算的。代码跑完如果输出不是 50 和 50,说明循环边界写错了,直接改。带着预期调试,比瞎猜快得多。
6. 第五题:九九乘法表,嵌套循环人人会写,格式才是分水岭
6.1 先保证逻辑正确:外层管行,内层管列
九九乘法表是最经典的双重循环练习题。外层变量 i 代表第几行,内层变量 j 代表这一行里乘到第几列。注意内层是 range(1, i+1),也就是第几行就只输出到第几个式子,这样才能形成左下三角。
for i in range(1, 10): for j in range(1, i + 1): print(f"{j} * {i} = {i * j}", end="\t") print()很多同学会把内层 range 写成 range(1, 10),结果每一行都输出 9 个式子,整体变成一个 9×9 的满表。严格来说不算错,但和常见“九九乘法表”形态不一样。如果题目要左下三角,内层终点就一定是 i+1,不是 10。
6.2 对齐方式决定了批改老师会不会一眼满意
end="\t" 是教材里常见的写法,制表符能让同一行各项勉强分开,但不同终端里制表符宽度不同,乘积是两位数后容易错位。更好的做法是控制每个输出的宽度:
for i in range(1, 10): for j in range(1, i + 1): print(f"{j} * {i} = {i * j:2d}", end=" ") print(){ i * j:2d } 的意思是让乘积占两个字符宽度,不够就在前面补空格。这样 11=1 和 23=6 这类一位数结果也能和两位数结果对齐。实测下来比制表符稳,交上去的截图也好看。
6.3 顺手做几个变式
如果实验只要求“打印九九乘法表”,左下三角通常就够了。想展示更多能力,可以把打印功能封装成函数:
def multiplication_table(rows=9): for i in range(1, rows + 1): for j in range(1, i + 1): print(f"{j} * {i} = {i * j:2d}", end=" ") print() multiplication_table()改成任意 rows 就能输出任意大小的乘法表。还可以把内层 range(1, i+1) 改成 range(1, 10),再把外层的 i 从 9 递减到 1,就能得到上三角版本的变式。这些扩展不复杂,但能在实验报告里写清楚“除了标准输出还做了什么尝试”,印象分会好不少。
7. 调试与避坑:这套实验我见过的新手翻车现场
7.1 报错信息速查表
写这套实验最常碰到的报错就这类,我整理成速查表,看到提示直接对照:
| 报错现象 | 原因 | 处理办法 |
|---|---|---|
| IndentationError: unexpected indent | 缩进层级不统一 | 全文件统一用 4 个空格,别混 Tab |
| TypeError: 'int' object is not iterable | 对数字直接用了 for 遍历 | 改成 for ... in range(...) 或先 str() |
| TypeError: 'int' object is not subscriptable | 把整数当下标取 | 先转成字符串再取位 |
| SyntaxError: invalid character '×' | 代码里混入中文符号或全角空格 | 代码部分全部改用英文标点 |
这些报错都是新手高频。看到后别急着删代码,先把提示的行号读出来,八成能直接定位。缩进问题最普遍,多重循环里内层代码一不小心缩进错位,判断条件就跑到循环外面去了,逻辑完全变味。
7.2 跑通程序后才发现结果的逻辑坑
比报错更隐蔽的是“程序完全能跑,但结果不对”。这种问题通常在几个固定位置。
素数题漏了 0 和 1 的前置判断,或者平方根边界写错,导致 25 被误判成素数;三位数题 if 里判断的是 i、j、k 互不相等,输出时却漏了某一项乘位,比如写成 i100+j10+k 却忘把个位算进去,结果输出两位数;四叶玫瑰数把 ** 写成 ^,程序不报错但结果必然错误,这是最冤的一种。这类逻辑错误很难靠眼睛看出来,最好的办法是拿一组已知答案去测,比如水仙花数只有 153、370、371、407,跑一遍看有没有全部命中。
7.3 我的排错习惯:先分步打印,再查边界
我调这类实验题的固定顺序是这样:先打印中间变量确认循环次数对不对。三位数那题先输出全部 64 个枚举结果,再打开 if 看筛选后的 24 个;素数那题单独调用 is_prime(9)、is_prime(17)、is_prime(25) 三个测试数,挨个确认返回结果;四叶玫瑰数哪个数错了,就把那个数的每一位和四次方打印出来逐项检查。
这个分步打印的习惯,比盯着代码干看有效得多。确认逻辑没问题之后,再删掉调试用的 print,保留一份最终版本提交。新手最容易犯的毛病是调试代码和正式提交代码混在一起,结果交上去的程序到处都是 print 中间变量,老师扣分你还没话说。
8. 顺手记录:我做这套题的顺序和交付习惯
我自己的做题顺序跟题号不一样:先把第五题九九乘法表写了,因为输出规律最直观,能迅速建立对嵌套循环的感觉;接着做第一题三位数组合,把循环加条件的组合方式练熟;再做统计数,把字符判断的几个方法记熟;然后才轮到素数,因为优化那块需要一点数学思维;四叶玫瑰数放最后,等前面循环和取位都顺了,难度自然降下来。顺序可以按个人情况调整,但原则都一样:从输出规律强的入手,逐渐过渡到需要抽象判断的题。
交付前我会做三件事:每个题都用 def 包成函数,哪怕实验没要求;文件名写成 exp1_p1.py 这种带题号的格式;本地跑两遍,一遍看运行结果,一遍故意输入边界值测试程序不会崩。这三件事帮我躲过不少交作业翻车的尴尬。
最后再给一个我自己很受益的做法:这套实验里的每一题都值得写两遍。第一遍按最直接的思路写,把功能先跑通;第二遍再压缩循环次数、改成列表推导式、把重复逻辑收进函数。第二遍刚开始会慢,但写完以后你对循环和条件判断的理解,比只抄一遍参考答案要扎实得多。