被问得最多的,永远是《Python语言程序设计基础》第3章程序部分的练习题答案。这一章恰好是很多人从“看得懂语法”到“写得出来程序”的分水岭。前两章背概念、抄代码还能应付,到了第3章,题目开始要求你自己拆问题、写逻辑、调错,很多人第一次在这里卡住。这篇博文把第3章里最常布置的程序题答案整理成一套能直接运行、带逐行解释的参考实现,覆盖水仙花数、九九乘法表、闰年判断、斐波那契数列、最大公约数、素数输出、猜数字、字符串统计和金字塔打印。所有代码只用Python标准库,任意支持Python 3的编辑器都能直接跑。适合刚学完基础语法、正在赶作业复习的同学,也适合已经写完但想对比写法的初学者。代码可以抄,但更重要的是看我为什么这么写、哪些位置容易踩坑。
1. 先说清楚:第3章程序题到底在练什么
1.1 这些题目为什么容易看懂却写不出来
很多初学者有同样的困惑:教材里每个知识点都认识,input、print、int()、if、for、while,单独拿出来都知道是干什么的,可真拿到一道“求水仙花数”“输出九九乘法表”的题,脑子就一片空白。这是因为语法知识和程序题之间缺了一层“拆解能力”。
程序题本质上是在考“把解决过程翻译成代码步骤”的能力。我经常用做饭来类比:语法相当于认识各种食材和调味料,但一道菜最终好不好吃,取决于你什么时候下锅、先炒什么、后放什么、火候怎么控。第3章的题就是这种“番茄炒蛋级”的练习,看着简单,但少一个步骤、顺序换一下,结果就完全不对。
拿水仙花数来说,很多人第一反应是“我知道要判断,但不知道怎么写”,原因就是没有先想清楚怎么从三位数里取出百位、十位、个位。取位这个动作,本质上就是整除和取余的组合。一旦你把“取百位”翻译成num // 100,把“取个位”翻译成num % 10,代码自然就写出来了。所以做这章题之前,先别急着打开编辑器,拿笔在纸上写三件事:输入是什么、处理过程分几步、输出是什么。这三件事写清楚了,代码只是翻译问题。
1.2 做题前必须准备好的思维工具箱
我自己做这章题时,会刻意准备一套固定的“思维工具包”。你可以在做题前先过一遍,后面每道题都往这几个方向靠。
第一,明确循环结构的选择。如果知道循环次数,比如“输出1到100之间”“输出9行”,优先用for;如果不知道次数,比如“一直猜直到猜对”“一直除直到余数为0”,就用while。这个选择不是死规矩,但能帮你少走弯路。
第二,警惕边界条件。第3章的题特别喜欢考边界:闰年判断里1900和2000的区别、九九乘法表里内层循环到i而不是永远到9、斐波那契数列里n=1和n=2的情况。很多代码看起来没问题,一测边界就废,就是因为做题时没有主动想“最特殊的情况是什么”。
第三,每写一个循环,先回答三个问题:循环变量是谁?每轮之后往哪走?什么时候停下来?这个习惯能从根源上避免死循环和循环次数错乱。我后面拆题时会反复用这套工具,你跟着过一遍就会感觉到,它比死记代码实用得多。
2. 基础经典题:参考答案与逐行拆解
2.1 水仙花数:取位运算第一次真正派上用场
题目要求通常是这样:输出100到999之间所有的水仙花数。所谓水仙花数,就是一个三位数,各位数字的立方之和等于它本身,比如153,因为 (1^3 + 5^3 + 3^3 = 153)。
这道题的核心不在“立方”或者“判断”,而在“怎么把三位数的每一位取出来”。百位最好办,直接用整除:num // 100,比如153除以100取整得到1。个位也简单,用取余:num % 10,153对10取余得到3。容易出错的是十位,很多新手会写num / 10 % 10,在Python 3里这得到的是浮点数,跟整数比较时容易出问题。正确写法是num // 10 % 10,先整除把数字变成两位数,再取余得到十位数。
参考代码:
for num in range(100, 1000): bai = num // 100 shi = num // 10 % 10 ge = num % 10 if bai ** 3 + shi ** 3 + ge ** 3 == num: print(num)运行后应该是四个数:153、370、371、407。
这里有一个特别常见的错误:把==写成=。=是赋值,==才是判断相等。如果你写成了if bai ** 3 + shi ** 3 + ge ** 3 = num:,解释器会直接报语法错误,报错位置通常在赋值号附近,看到类似报错先检查这里。 另外,判断立方时个人更推荐**而不是连乘三个数,因为可读性好,也少写括号。
2.2 九九乘法表:嵌套循环不是靠死记
九九乘法表的经典问法是输出左下三角结构,第一行1个算式,第二行2个算式,直到第九行9个算式。这道题几乎必考,同时也很容易写到一半就跑偏。
核心思路是两层循环:外层循环控制行,从1到9;内层循环控制每一行里打印几个算式。内层的循环范围不是固定的1到9,而是从1到当前行号i。这一步想通了,整道题就通了。
for i in range(1, 10): for j in range(1, i + 1): print(f"{j}x{i}={i * j}", end="\t") print()这里有两个细节值得注意。第一,print(f"{j}x{i}={i * j}", end="\t")里的end="\t"表示每个算式之间用制表符分隔,而不是默认的换行。很多新手把end忘了,结果一个算式占一行,整张表变成一列。第二,内层循环结束后,必须有一个不带end参数的print()来换行,否则所有行都会挤在同一行里。这个换行的print()要放在内层循环外面、外层循环里面,位置错了输出就会乱。
用f-string格式化时,{j}x{i}={i * j}输出的是“1x1=1”“1x2=2”这样的格式。如果你的运行环境是Python 3.6以下,不支持f-string,可以改成print(str(j) + "x" + str(i) + "=" + str(i * j), end="\t")或者print("{}x{}={}".format(j, i, i * j), end="\t")。 但我还是建议直接换到Python 3.x新版本,这种写法简洁得多。
我见过很多人在这一题上反复改错,最后发现是外层range(1, 10)写成了range(10),导致多出一行0开头的算式,或者i + 1写成了i,导致每一行都少最后一个算式。这种边界问题,做的时候一定要心里默念:循环范围取不到终点,想包含终点必须+1。
2.3 判断闰年:逻辑优先级是这道题的唯一难点
判断闰年的规则大家都知道:能被4整除但不能被100整除,或者能被400整除。写成代码只有一行判断,但这一行里藏着这章最容易忽视的逻辑优先级问题。
year = int(input("请输入年份:")) if (year % 4 == 0 and year % 100 != 0) or (year % 400 == 0): print(year, "是闰年") else: print(year, "不是闰年")我建议在and和or组合的时候,把括号写清楚。虽然Python中and的优先级高于or,即使不写括号,year % 4 == 0 and year % 100 != 0 or year % 400 == 0也会按你期望的顺序执行,但写成括号之后读起来更直观,也不容易在多种条件下搞混。为了省一个字符而牺牲可读性,不值得。
这一题的正确性必须用边界年份验证:1900年能被4整除但不能被400整除,所以不是闰年;2000年能被400整除,是闰年;2024年能被4整除且不能被100整除,是闰年。如果程序把1900判断成闰年,说明条件里丢掉了一半;如果把2000判断成不是闰年,说明把“能被400整除”的条件写到了and里。用这三个年份一测,题目基本就能下结论了。
另外有一点要注意:year % 100 != 0是“不能被100整除”的意思,不要写成not year % 100 == 0,虽然逻辑等价,但可读性很差。代码是给人看的,尽量写直观的判断条件。
3. 算法递进题:循环、递推与函数的组合应用
3.1 斐波那契数列:前n项怎么输出最干净
斐波那契数列的题目很常见,但版本很多,有的是求第n项,有的是输出前n项。我先给前n项版本的参考答案,因为它的写法更经典,也能讲清递推思想。
斐波那契数列的递推关系是:第一项和第二项都是1,从第三项开始,每一项等于前两项之和。所以 (1, 1, 2, 3, 5, 8, 13, ...)
n = int(input("请输入要输出的项数:")) if n <= 0: print("n 必须是正整数") else: a, b = 1, 1 for _ in range(n): print(a, end=" ") a, b = b, a + b print()这段代码的核心是a, b = b, a + b。这一行的意思是:先把a赋值为原来的b,再把b赋值为原来的a + b。两个赋值是同时完成的,Python会先算右边的两个值,再分别赋给左边的变量。
这里有一个几乎所有初学者都会踩的坑:如果你习惯性地拆成两行写:
a = b b = a + b那b就变成了b + b,整个数列会变成 (1, 1, 2, 4, 8, 16...),完全错误。原因是第二行执行时,a已经不是原来的a,而是被上一句改成了b。所以我每次讲到这道题都会特别强调:递推更新需要“同时拿到旧值”,Python的元组赋值就是为了解决这种问题。除了实现细节,你还可以观察一下为什么循环里能直接用print(a):因为a在每轮刚开始时正好是“当前这一项”,打印完再推下一个,循环次数正好等于项数。
如果题目要求的是“求第n项”,可以改成下面的写法:
n = int(input("请输入 n:")) if n <= 0: print("n 必须是正整数") elif n == 1 or n == 2: print(1) else: a, b = 1, 1 for _ in range(n - 2): a, b = b, a + b print(b)这里循环n - 2次,是因为第一项、第二项已经直接确定了,从第三项开始递推,循环正好推进到第n项。
3.2 最大公约数:辗转相除比暴力遍历好在哪
最大公约数的题目,最容易想到的解法是从较小的那个数开始往下降,找到第一个能同时整除两个数的数字。比如求12和8的最大公约数,从8开始试,8不行,7不行,6不行,5不行,4可以,答案是4。这种方法叫暴力遍历,对小数字没问题,但遇到两个比较大的数,效率肉眼可见地差。
教材第3章更想让你体会的是辗转相除法。它的原理用一句话说:两个数的最大公约数,等于较小数和“较大数除以较小数的余数”的最大公约数。用数学式子写就是 (gcd(a, b) = gcd(b, a % b)),不断套用,直到余数为0,最后一个非零余数就是答案。
a = int(input("请输入第一个数:")) b = int(input("请输入第二个数:")) x, y = abs(a), abs(b) while y != 0: x, y = y, x % y print(f"最大公约数是:{x}")代码里的while y != 0是这道题最容易绕晕的地方。每一轮循环里,原来的y变成新的x,原来的x % y变成新的y。可以拿18和24手动跑一遍:第一轮x=24, y=18,18不等于0,执行完变成x=18, y=6;第二轮x=18, y=6,6不等于0,执行完变成x=6, y=0;第三轮y=0,循环结束,输出x=6。答案正确。
为什么用abs()?因为教材题目一般只给正整数,但万一遇到负数输入,直接取余在Python里的结果会让你困惑。先转换成绝对值,保证计算过程全部在非负整数上进行。如果两个数都是0,数学上没有意义,这个程序会输出0,所以做输入校验时最好提醒自己限制为正整数,也可以循环判断直到用户输入合法为止。
3.3 素数输出:范围优化与函数封装
素数题有各种问法,最常见的是“输出2到N之间所有的素数”。这道题能同时练到循环、判断和函数封装,属于第3章里综合度较高的一道。
先说判断素数的朴素做法:一个数如果只能被1和它本身整除,就是素数。判断时从2试到这个数本身减1,有一个能整除就说明不是素数。但这样试到n-1太浪费了。优化点是:不需要试到n-1,只需要试到 (\sqrt{n}) 就够了。原因是:如果 (n = a \times b),那么 (a) 和 (b) 中一定有一个不大于 (\sqrt{n}),如果存在因子,在 (\sqrt{n}) 之前就能找到。
import math def is_prime(num): if num < 2: return False if num == 2: return True if num % 2 == 0: return False limit = int(math.sqrt(num)) + 1 for i in range(3, limit, 2): if num % i == 0: return False return True n = int(input("请输入一个正整数:")) for num in range(2, n + 1): if is_prime(num): print(num, end=" ") print()这里做了几个层次的处理。先排除小于2的数和偶数,再单独处理2,因为2是唯一的偶数素数,也正好是素数判断的起点。处理完这些特殊情况后,循环从3开始,步长为2,只测试奇数,循环次数进一步减半。
如果你还没学到函数,也没关系,可以把is_prime内部的逻辑直接展开在主循环里,但代码会显得很乱。我更建议把“判断一个数是否为素数”封装成一个函数,因为输出素数这个任务本身就由两层逻辑组成:外层遍历数字,内层判断属性。用函数把内层逻辑独立出来,主程序就一行if is_prime(num),读起来清爽得多。等学到后面章节,你会越来越依赖这种“拆成小函数”的思维。
4. 综合实战题:随机数、字符处理与循环嵌套
4.1 猜数字游戏:循环条件与输入校验的配合
猜数字是综合题里的熟面孔:程序先生成一个1到100的随机整数,用户去猜,程序提示“大了”或“小了”,猜对后显示猜的次数。这题既练while循环的“不确定次数”结构,也练输入处理和比较判断。
import random target = random.randint(1, 100) count = 0 print("我想好了一个 1 到 100 之间的整数,开始猜吧。") while True: text = input("请输入你的猜测:") if not text.isdigit(): print("请输入正整数") continue guess = int(text) count += 1 if guess < target: print("太小了,再大一点") elif guess > target: print("太大了,再小一点") else: print("你猜对了!一共猜了", count, "次") breakrandom.randint(1, 100)生成的是包含1和100在内的随机整数,很多初学者会写成random.randrange(1, 100),那样100就永远不会出现了。这是非常典型的边界错误,值得记一下。
循环结构上,因为不知道用户要猜多少次,我用的是while True加break的写法。每猜一次,count += 1就必须执行一次。这句话的位置不要放在判断猜对之后再执行,否则正确的那一次不会被计入。我在调试类似代码时经常看到这种情况,最终答案往往是“次数少了一次”。
代码里还加了一层输入校验:text.isdigit()用来判断用户输入的字符串是不是全由数字组成。如果不是,就提示用户重新输入,并用continue跳回循环开头,不增加猜的次数。如果没有这一步,用户输入一个非数字字符,int(text)会直接抛出异常。教材第3章可能还没教异常处理,所以isdigit()是现阶段比较友好的防错办法。
4.2 字符串统计:字符类型的隐藏细节
字符串统计题通常这样出:输入一行字符,统计其中英文字母、数字、空格和其他字符的个数。题目不难,但有几个隐藏细节特别容易让“看起来对”的代码实际出错。
比较稳妥的写法是:
text = input("请输入一行字符串:") letters = digits = spaces = others = 0 for ch in text: if ('a' <= ch <= 'z') or ('A' <= ch <= 'Z'): letters += 1 elif '0' <= ch <= '9': digits += 1 elif ch == " ": spaces += 1 else: others += 1 print("英文字母个数:", letters) print("数字个数:", digits) print("空格个数:", spaces) print("其他字符个数:", others)为什么我不直接推荐ch.isalpha()和ch.isdigit()?因为Python的isalpha()是按Unicode字母判断的,汉字也会被判为“字母”。如果题目要求统计“英文字母”,用isalpha()统计出来的数字会把中文也算进去,一看就错。同理,isdigit()对全角数字、上标数字等Unicode数字字符也可能返回True,比如'²'.isdigit()结果是True。所以当题目明确要求“英文字母”和“数字字符”时,直接用字符范围比较更符合题目的语义。
空格判断我用的是ch == " "而不是ch.isspace(),因为isspace()会把制表符\t、换行符\n等全部算进去,如果题目只是让统计空格,用空格字面量最准确。当然,如果老师要求所有空白字符都算“空格”,那用isspace()也说得过去。做题时先看清题目要求,这个习惯比代码本身重要。
4.3 金字塔打印:嵌套循环的变式训练
九九乘法表练了嵌套循环,金字塔打印是同一类思想的进阶变式。常见题目是:输入层数n,输出一个由星号组成的等腰三角形。
观察规律是这道题的关键。第1行有1个星号,第2行有3个星号,第3行有5个星号。第 (i) 行星号数量是 (2i - 1)。为了让三角形居中,第 (i) 行前面还要放 (n - i) 个空格。
n = int(input("请输入层数:")) for i in range(1, n + 1): print(" " * (n - i) + "*" * (2 * i - 1))这个代码非常短,但如果不理解“空格数加星号数”的规律,第二反应就是想着用两层循环去逐个打印字符。实际上Python的字符串乘法" " * (n - i)可以直接生成指定数量的空格,整个输出一下子变得很干净。等学到字符串这一章节,你会觉得这个操作很自然,但在第3章里能主动用上,说明你已经有了“先观察规律,再选工具”的意识。
如果题目要求的是靠左的直角三角形,那就更简单:
n = int(input("请输入层数:")) for i in range(1, n + 1): print("*" * i)这两道变式题的背后都是同一个思想:把“每行输出什么”抽象成一个与行号 (i) 有关的表达式,而不是机械地一层层写循环。
5. 运行报错排查与作业避坑速查
5.1 缩进、类型、死循环三类高频报错
每次一到第3章练习,群里问报错最多的问题基本可以归成三类:缩进错误、类型错误、死循环。我挨个说,建议你对照自查。
第一类是缩进错误。Python用缩进表示代码块,if、for、while下面一行如果没缩进,解释器会直接报IndentationError。更隐蔽的是混用Tab和空格,明明看起来对齐了,一运行就报错。我的建议是:编辑器统一设置成“把Tab自动转换成4个空格”,然后整个文件只用缩进,不用Tab。绝大多数现代编辑器默认都这么干,但如果你在写作业时从网页复制代码,最容易把Tab混进来。遇到缩进报错,先把该代码块全部选中,用编辑器转换成空格。
第二类是类型错误。这种错误通常不是解释器直接报TypeError,而是逻辑悄悄错。比如input()返回的是字符串,直接拿去和整数比较,Python 3里两个类型不同,>会抛TypeError: '>' not supported。但更隐蔽的是第2.1节提到的情况:用/而不是//取十位,得到浮点数,导致后面的立方和比较结果不对。遇到类型问题,先搞清楚每个变量的类型,必要时用type()打印出来看一下。
第三类是死循环。常见场景有:while循环里忘了更新循环变量,比如做最大公约数时少了x, y = y, x % y,程序卡住不动;猜数字循环里忘了break,猜对了也不跳出;还有循环条件是while True但没有出口。调试时如果程序一直不结束,先按Ctrl+C强制中断,然后检查循环体里是否有让条件最终变为False的语句。
5.2 常见问题速查表
第3章的程序题来来去去就是那几个坑,我整理了一张速查表,做完题对照检查一遍,比自己瞎试快得多。
| 问题现象 | 常见原因 | 解决方法 |
|---|---|---|
IndentationError: unexpected indent | Tab与空格混用,或不该缩进的地方缩进了 | 全选代码统一转成空格,检查代码块层级 |
TypeError: '>' not supported between instances of 'str' and 'int' | input()返回值没有转成整数 | 用int(input(...))包裹 |
| 水仙花数十位算错 | 用了/而不是// | 十位写成num // 10 % 10 |
| 九九乘法表每个算式都换行 | 内层print没写end参数 | 加end="\t" |
| 九九乘法表每行缺最后一个算式 | 内层循环范围写成range(1, i) | 改成range(1, i + 1) |
| 闰年判断1900年输出是闰年 | 漏了“不能被100整除”的条件 | 用完整规则year % 4 == 0 and year % 100 != 0 |
| 斐波那契数列输出变成1、1、2、4、8 | 把a, b = b, a + b拆成两行赋值 | 用元组同时赋值,或先保存旧值 |
| 素数判断把4也算成素数 | 循环范围没加上+1,导致sqrt(4)开根后只试到2 | 范围写int(math.sqrt(num)) + 1 |
| 猜数字猜对了但次数少1 | count += 1放在break之后 | 把计数语句放在判断之前 |
| 字符串统计中文字符被算成字母 | 用isalpha()判断英文字母 | 改用'a' <= ch <= 'z' or 'A' <= ch <= 'Z' |
这张表不可能覆盖所有报错,但覆盖了第3章练习题里出现频率最高的几种。如果你遇到不在这张表里的问题,我强烈建议你用最笨的办法:把关键变量用print()打出来,看每一步的值是否符合预期。程序就是一步一步跑出来的,打印中间结果永远是最直接的调试方式。
5.3 我的做题习惯和复查顺序
最后分享一下我自己做这章题时的复查习惯。很多人写完代码,运行一次看到输出没问题就交作业了,但程序题的边界往往不是第一眼能看出来的。我的复查顺序很固定。
第一步,跑一遍标准答案。水仙花数要看到153、370、371、407四个结果;九九乘法表要数一下第一行一个算式、第九行九个算式;闰年判断要分别输入1900、2000、2024。第二步,测边界值。斐波那契分别输入1、2、10;素数输出分别输入2、3、4;金字塔分别输入1、5。这些边界值最容易暴露问题。第三步,如果时间充裕,我会把同一道题换一种写法再实现一遍,比如把for改成while,或者把判断逻辑封装成函数。换一种写法不是为了炫技,而是逼自己重新梳理一遍逻辑。很多时候你觉得代码“能跑了”,其实只是碰巧输入没有踩到雷,换一种写法就会发现自己对这道题的理解还不够透彻。
这个习惯说起来简单,但确实帮我避免了很多次“交作业时没问题,上课展示时当场翻车”的尴尬。你可以挑一两道题试试,尤其挑那些你做的时候有点犹豫的题。
说回第3章本身。这些练习题看起来基础,但它们真正想训练的是你对“输入、处理、输出”这一整套流程的把控能力。后面学到列表、字典、字符串方法时,你会发现第3章练出来的循环思维和调试敏感度,比任何一道具体的答案都值钱。所以就算你现在的代码还写得不漂亮,只要能自己动手把每一题跑通、想明白为什么错,就已经达到这一章的核心目标了。