简介:这份《Python语言程序设计(刘卫国)实验指导》部分答案文档,面向正在学习Python基础编程的本科生或自学者,适合配套教材实验环节使用。内容覆盖实验1至实验4、实验6与实验7的典型题目,并涉及实验8字典与集合的部分题目,包括math库计算π²、顺序结构中的格式化输出与整数小数分离、选择结构中的条件判断与浮点数比较陷阱、循环结构中的素数筛选与嵌套循环求和,以及字符串正则、列表与元组的增删操作等;部分题目还补充了代码注释和排错思路,例如元组不可变导致append报错时应改用列表,或浮点比较需先round舍入。包体为1个doc文件,大小约145KB,纯文档形式,打开即可直接阅读,适合在完成实验后对照检查、核对输出结果或快速定位常见错误。该资源已有6689人学习,使用频率较高,是Python实验课的实用参考。
1. 这份Python实验指导答案能帮你什么:从「填①」到「跑出结果」的完整闭环
这份Python实验指导覆盖了从基础语法到tkinter图形界面设计的14个实验,每个实验都给了可运行答案,像实验1里计算π²、实验7里列表与元组的报错修复,都是新手最容易卡半小时的细节。它的价值不只是让你照抄①里的内容,而是把每个程序运行后应该出现的输出、会踩的异常类型全部摆了出来,适合跟着教材学但缺一个“对答案”入口的人,也适合想在上机前把运行结果推演一遍的人。
拿到这份资料后我建议你做一件事:不要只盯着答案文本,每道题都亲手跑一遍。文档里有些输出存在排版压缩,比如循环实验的结果被印成紧凑的一串,实际运行是三行;还有些题目的标准答案本身就值得商榷。下面按实验顺序把值得展开的题拆开讲,重点放在“为什么是这个答案”和“哪里最容易翻车”上。
2. 从π²到整除判断:实验1~3的代码补全与格式化细节
2.1 实验1:math.pi 不是唯一答案,难点在为什么选这个库
实验1第6题就两行代码:import math 后补全 p,让程序输出π²。答案是 math.pi * math.pi。如果你第一时间想到的是 3.1415926**2 或者 math.pow(math.pi, 2),结果也对,但这份答案选 math.pi 相乘是有原因的——math.pi 是双精度浮点常量,精度比你自己手敲圆周率高得多,后续参与复杂计算时误差更小。
import math p = math.pi * math.pi print(p)这段代码说明了 Python 标准库 math 的两种常用风格:直接引用常量 math.pi,以及调用 math.pow 做幂运算。两者的区别在于 math.pow 适合动态指数,比如指数来自某个变量;而两个 math.pi 相乘适合固定平方的场景。我一般处理这类题目会直接写乘法,少一次函数调用,逻辑也更直观。对于新手,通过这道题还能顺带理解 import 的作用范围:import math 之后要用 math.pi 这种带前缀的方式访问,而不是直接写 pi。
2.2 实验2:eval 输入的危险与格式化参数的作用
实验2第3题要求输入出生年、月、日,再按指定格式输出。答案是先用 eval(input()) 一次接收三个值,再用 format 格式化输出。这里有两个值得展开的点。
y, m, d = eval(input()) print("我的出生日期是{0}年{1:02d}月{2:02d}日".format(y, m, d))eval 在这里把输入的 “1992,12,5” 当做一个元组表达式解析,再解包给 y、m、d。这是教材里常见的偷懒写法,能省去 split 和 int 转换,但实际项目中我不建议对用户输入直接用 eval,因为任何合法 Python 表达式都会被执行,存在安全隐患。做实验答案时可以这么写,理解它等价于 y, m, d = tuple(map(int, input().split(','))) 即可。
格式化串里的参数含义如下表:
| 占位符 | 含义 | 示例输入 | 输出 |
|---|---|---|---|
| {0} | 第0个参数原样输出 | 1992 | 1992 |
| {1:02d} | 第1个参数按十进制整数输出,占2位,左补0 | 12 | 12 |
| {2:02d} | 第2个参数按十进制整数输出,占2位,左补0 | 5 | 05 |
第4题分离整数部分和小数部分,用的 k=int(x)、y=x-k,题目限定“正的实数”是有原因的:int() 对正数是向下取整,对负数是向零取整,行为不一致。我一般会用 math.modf 一步到位,但这份答案强调基础写法,拆成两步更容易理解。
2.3 实验3:浮点数比较的坑,if x-y==0.1 为什么不相等
实验3第2题是最经典的浮点精度陷阱之一。x=2.1,y=2.0,x-y 在计算机里并不是精确的 0.1,而是 0.10000000000000009,所以直接比较会输出 Not Equal。这不是 Python 的 bug,而是所有用二进制表示浮点数的语言的通病。
x = 2.1 y = 2.0 if abs(x - y - 0.1) < 1e-9: print("Equal") else: print("Not Equal")常见的修正方案有两种:一是用 abs(x-y-0.1) < 1e-9 做绝对误差判断,适合这里的具体数值;二是 round(x-y, 1) == 0.1 比较,精度由第二个参数控制。实际工作中我更推荐前者,因为 round 的结果在边界情况下仍可能受表示误差影响。从这份答案里最值得带走的一条经验是:涉及小数比较,别直接写 ==,永远留一个容差。
第3题补全 if m%3==0 or m%7==0,考察的是取模运算和 or 连接两个条件。注意取模运算符 % 的优先级高于 ==,所以 m%3==0 不需要额外加括号。输出 “Yes” 和 “No” 的大小写不要改,批改系统通常区分大小写。
3. 循环、列表与字典:实验4、7、8里最难抄对的三类题
3.1 实验4:while 循环的陷阱与素数输出的哨兵法
实验4第1题给了个很短的循环,文档里把运行结果印成 “135”,这个排版容易误导人。实际逐行推演:i=1 时 while i+1 为真,i>4 不成立,打印 1,i 变成 3;再次进入循环打印 3,i 变成 5;第三次进入时 i>4 成立,打印 5,i 加 1 变成 6,break 退出。所以真实输出是三行:1、3、5。
i = 1 while i + 1: if i > 4: print(i) i += 1 break print(i) i += 2这里有两个细节。第一,while i+1 在 Python 里等价于 while True,因为任何非零整数都是真值;这个条件永远不会因为 i 变化而变成假,退出只能靠 break。第二,文档里把输出排成一行 “135”,是按 print 默认换行的逻辑推不出来这个结果的,只有把 print 的 end 参数改成 '' 才能排成一行。所以凡是看到这种紧凑输出,建议自己先跑一遍确认。
实验4第3题是找 100 到 200 之间的素数,每行输出 10 个。程序里用了一个“哨兵”写法:for 循环结束后 if i == k+1 判断循环是否完整跑完。
import math n = 0 for m in range(101, 201, 2): k = int(math.sqrt(m)) for i in range(2, k + 2): if m % i == 0: break if i == k + 1: if n % 10 == 0: print() print(m, end=' ') n += 1这段代码的核心是 range(2, k+2) 这个区间:k 是 m 平方根的整数部分,只需要试除到 sqrt(m) 就能判断素数,这是数学上的结论。range(2, k+2) 确保能试到 k+1,配合后面的 if i == k+1 判断循环是否正常结束。如果中间 break 了,说明有因子,i 不会等于 k+1。这种哨兵法在旧版教材里很常见,现代 Python 更推荐用 for...else 结构,语义更清晰,但作为考试题答案,这个写法能跑,而且没有任何毛病。
3.2 实验7:元组不可变引发的 AttributeError,以及筛选法求素数
实验7第2题是这份答案里最典型的报错题之一。程序企图用 p=() 创建一个空元组,然后循环 append 数据,运行后直接报 AttributeError: 'tuple' object has no attribute 'append'。
p = [] # 原来是 p = () for i in range(10): x = int(input()) p.append(x) print(p)问题根源是元组不可变,没有 append 方法。改法有两个:把 p=() 改成 p=[],这是最直接的;或者保持元组风格,用 p = p + (x,) 每次重建元组,但这样性能差、代码绕。我遇到这种情况一律改列表,因为后续大概率还要做排序、切片等操作。元组的不可变特性适合做字典键或防止意外修改,不适合做动态收集数据的容器。
实验7第3题是筛选法求 [2,n] 范围内的素数,程序已经写好了框架,要补两个空。
from math import * n = int(input("请输入 n:")) m = int(sqrt(n)) p = [i for i in range(n + 1)] for i in range(2, m + 1): if p[i]: for j in range(2 * i, n + 1, i): # 去掉 i 的倍数 p[j] = 0 for i in range(2, n + 1): if p[i] != 0: print(p[i])第一个空填 i,表示步长等于当前筛的素数本身,这样能精确覆盖所有 i 的倍数;第二个空填 p[i] 或 p[i]!=0 都可以。筛选法理论依据是:合数必有小于等于自身平方根的质因子,所以只需筛到 sqrt(n) 为止,这也是 m=int(sqrt(n)) 的来由。这个算法的时间复杂度接近 O(n log log n),比逐个试除快得多,是数据量稍大时的首选方案。
3.3 实验8:字典的元组键与集合运算的输出顺序
实验8第2题用元组做字典键,numbers={(1,2,3):1, (2,1):2, (1,2):3},输出 len=3、sum=6。这里值得解释的是三个键互不相同,因为元组会按内容逐一比较。如果把键改成列表,程序会在插入时报 TypeError,因为列表不可哈希,这正好能和上一题元组可哈希的特性对应起来。
numbers = {} numbers[(1, 2, 3)] = 1 numbers[(2, 1)] = 2 numbers[(1, 2)] = 3 print(len(numbers), sum(numbers.values())) print(numbers)注意字典的遍历顺序:Python 3.7 之后字典保持插入顺序,但在老版本里顺序不保证。文档答案里 print(numbers) 的顺序在不同环境可能不同,这不是程序错误,是解释器版本差异。判断这类题的对错要看键值内容,不要看打印顺序。
实验8第3题考察集合运算,a=set('ababcdabca') 会得到字符集合 {a, b, c, d},x 是 a 中去掉 a 和 b 后的元素集合,也就是 {c, d}。差集 a-x 得到 {a, b},并集得到 {a, b, c, d},对称差集结果相同,交集得到 {c, d}。集合是无序的,print 出来顺序依赖哈希值,不同机器可能不同,但要记住 a^x 和 a|x 在 x 是 a 子集时结果相同,这是集合运算的减法性质。
4. 函数、递归与面向对象:运行结果是推出来的,不是背出来的
4.1 实验9:函数返回值与递归边界,foo(num) 的素数判断漏洞
实验9第1题打印了一小段函数,文档答案写的是 17,但你按原代码逐行推会发现结果不是 17,这里有个容易被忽略的逻辑漏洞。
def foo(num): for j in range(2, num // 2 + 1): if num % j == 0: return False else: return True def main(): n, c = 8, 0 for i in range(2, n + 1): if foo(i): c += i print(c) if __name__ == '__main__': main()问题出在 foo 的 for 循环在第一次迭代就必然 return,无论条件成立与否。以 num=5 为例,j 取 2,5%2 不等于 0,走 else 返回 True;以 num=2 为例,range(2, 2) 为空,循环体不执行,函数没有 return 语句,返回 None,if foo(2) 视为 False。推演 2 到 8 的结果:2 和 3 返回 None,4、6、8 返回 False,5、7 返回 True,所以 c=5+7=12。要得到文档里的 17,需要把 foo 修正成真正的素数判断:for 循环里只判断能否整除,能整除就 return False,循环结束后 return True。修正后 2、3、5、7 都会被计入,c=17。
这个例子恰恰暴露了核心问题:直接抄答案只能拿到“能跑的代码”,拿不到“正确的逻辑”。我建议遇到这类题,把代码敲进解释器跑一遍,看看自己环境的输出是不是和文档一致,不一致时优先怀疑代码逻辑,而不是怀疑机器。
实验9第2题是递归生成斐波那契数列,foo(mylist, 10) 最终得到 [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]。
def foo(list, num): if num == 1: list.append(0) elif num == 2: foo(list, 1) list.append(1) elif num > 2: foo(list, num - 1) list.append(list[-1] + list[-2])递归边界是 num==1 追加 0,num==2 先递归到 num==1 追加 0,再追加 1。从 num=10 开始逐层递减,每次追加的数是当前列表最后两项之和。这个递归深度是 O(n),num=10 没问题;如果改成 foo(list, 1000),Python 默认递归深度限制约 1000 层,会直接抛 RecursionError。遇到这种情况我会先把递归改成迭代,用一个循环维护最后两个值,效率和可读性都更好。
4.2 实验10:多重继承下的 MRO,以及 Circle 类的周长面积计算
实验10第1题是多重继承中最容易让人混淆的一道题。C1(P1, P2)、C2(P1, P2)、GC(C1, C2),实例 gc 调用 foo 输出 called P1-foo,调用 bar 输出 called C2-bar()。
class P1: def foo(self): print('called P1-foo') class P2: def foo(self): print('called P2-foo') def bar(self): print('called P2-bar') class C1(P1, P2): pass class C2(P1, P2): def bar(self): print('called C2-bar()') class GC(C1, C2): pass要理解这个输出,需要知道 Python 的 C3 线性化算法确定的方法搜索顺序。GC 的 MRO 是 GC -> C1 -> C2 -> P1 -> P2 -> object。foo 的搜索路径:GC 没有 foo,C1 没有 foo,C2 没有 foo,P1 有 foo,所以输出 called P1-foo。bar 的搜索路径:GC 没有,C1 没有,C2 有 bar,输出 called C2-bar()。我碰到这种题不会手推,直接在交互环境里执行 print(GC.mro),一次就能看到完整顺序。MRO 的细节对日常写业务代码够用就好,但如果你在写框架或库,一定要意识到父类顺序会直接影响方法覆盖。
实验10第3题是定义一个 Circle 类,根据半径求周长和面积,要求补全构造函数。
import math class Circle: def __init__(self, radius=5): self.radius = radius def getPerimeter(self): return 2 * math.pi * self.radius def getArea(self): return math.pi * self.radius * self.radius c1 = Circle() c2 = Circle(10) print(c1.radius, c1.getPerimeter(), c1.getArea()) print(c2.radius, c2.getPerimeter(), c2.getArea())这里有两个点值得注意。第一,init是实例初始化方法,创建对象时自动调用,默认参数 radius=5 让 c1=Circle() 可以直接用默认值,这是常见的构造器设计。第二,默认参数如果换成可变类型,比如 [] 或 {},会在多次调用间共享同一个对象,导致状态污染。这里 radius 是数字,没有这个问题,但我要提醒你:看到默认参数是列表、字典这类可变对象时,默认值要在函数内部用 None 占位,再在函数体里初始化。
5. 文件、异常与正则:常见报错排查清单
5.1 实验6:字符串切片与正则全量匹配的边界
实验6第1题是字符串连接与切片复制。c='123'+'456'+'789' 先拼出 '123456789',c[-3:] 取最后三位 '789',乘以 2 得到 '789789',追加后最终结果是 '123456789789789'。文档里有的版本把它印成另一串数字,看起来像是排版错位。遇到这种情况,你自己在解释器里敲一遍就能得到正确答案,这也解释了为什么我坚持“答案文档只能当参考”。
c = '123' + '456' + '789' c += c[-3:] * 2 print(c)字符串切片 c[-3:] 用负索引表示从末尾往前数 3 位,这在处理定长尾部信息时非常实用。我一般用它取文件扩展名或日志最后几行,比先算长度再切片可读性好。
实验6第8题用正则判断字符串是否只有小写字母或数字,答案是 re.match('^[0-9a-z]+$', s)。这里有一个边界情况:$ 在正则里默认匹配字符串末尾或末尾换行符前的位置。如果 s='12345abc\n',不管加不加 $ 都可能匹配出意外结果。更稳妥的写法是 re.fullmatch('[0-9a-z]+', s),它强制整个字符串参与匹配,不需要手动写 ^ 和 $。正则的边界是新手最容易翻车的地方,建议默认用 fullmatch 代替 match 加锚点。
5.2 实验11:文件读写模式 w 与 seek(0) 的真实作用
实验11第1题把 0 到 9 写入 a.dat,再读出来求和,结果是 45。实验11第2题换成了 w+ 模式,写入 1 到 9 后直接 read(),这里隐藏了一个必踩的坑。
fo = open("file.txt", "w+") for i in range(1, 10): fo.write(str(i)) fo.seek(0) ls = fo.read() fo.close()w+ 表示可读可写,但写入后文件指针停在末尾,直接 read() 读到的必然是空字符串。fo.seek(0) 把指针挪回文件开头,read() 才能读到完整内容。这是文件操作里最经典的一行“后悔药”。如果不写 seek(0),后续求和就是 0,而且不会报错,排查起来得花时间想为什么数据丢了。文件读写完成后及时 close() 是好习惯,但我更推荐用 with open(...) as f 的上下文管理器,异常时自动关闭文件,省心。
实验11第3题是复制文件并把小写转大写,答案补全是 open("sample_copy.txt", 'w') 和 f2.write(line.upper())。
f = open("sample.txt") L1 = f.readlines() f2 = open("sample_copy.txt", 'w') for line in L1: f2.write(line.upper()) f.close() f2.close()readlines() 返回的每一行都带着换行符,upper() 只转换字母大小写,不影响换行,所以目标文件的换行格式能和原文件保持一致。这里 w 模式每次运行会覆盖目标文件;如果想保留多次运行的历史结果,我一般改文件名加时间戳,或者用 a 追加模式。
5.3 五条高频踩坑记录:现象、原因、解决
以下五条是从整套实验里提炼出的报错模式,每条都值得记下来。
一、AttributeError: 'tuple' object has no attribute 'append' 现象:实验7第2题,创建空元组后循环添加元素,运行直接报错。 原因:元组是不可变序列,本身没有 append 方法。 解决:初始化为空列表 p=[];如果必须用元组,改成 p = p + (x,),但这样每次都会创建新元组,不推荐。
二、RecursionError: maximum recursion depth exceeded 现象:实验9第2题把递归参数 num 改大,程序崩溃。 原因:Python 默认递归深度限制约 1000,递归式和尾递归都不做优化,深度过大就会触发保护。 解决:改成循环迭代,或用 functools.lru_cache 缓存中间结果。把 setrecursionlimit 调大只是临时缓解,治标不治本。
三、浮点数比较结果不符合预期 现象:实验3第2题,x-y 与 0.1 比较输出 Not Equal。 原因:浮点数用二进制近似表示,2.1-2.0 实际是 0.10000000000000009。 解决:用 abs(x-y-0.1) < 1e-9 做容差比较,或者先 round 到固定精度再比。
四、读文件总是拿到空字符串 现象:实验11第2题,write 后立刻 read,返回空。 原因:写操作后文件指针在末尾,read 从当前位置读,没有内容。 解决:读写切换之前执行 fo.seek(0),或用 with open 分别以写和读模式打开两次。
五、正则匹配多行文本失败 现象:实验6第8题,字符串末尾多一个换行符后匹配不上。 原因:$ 的默认语义匹配末尾换行前的位置,match 又只从头开始,边界把结果搞乱了。 解决:用 re.fullmatch('[0-9a-z]+', s),它要求整个字符串完全匹配,不受锚点影响。
这五条覆盖了类型错误、递归深度、浮点精度、文件指针、正则边界五类问题。碰到类似报错时,先对照现象找原因,再按解决方式改,基本都能一次跑通。
6. 进阶验证:把实验答案当调试素材,跑通再改成自己的小工具
实验13到实验15进入图形界面阶段,这部分最有价值的是猜数字游戏完整参考程序,它把随机数生成、事件绑定、二分检索思想全部揉进了 tkinter 界面里。
实验13的彩条程序用 create_rectangle 在 Canvas 上画矩形,x0 每次加 30,矩形宽度 15、间距 15,铺满 10 条。如果你要改成其他颜色或宽度,只需要改 fill 和步进值。Canvas 的坐标体系是左上角原点,x 向右增大,y 向下增大,这和数学坐标系相反,画图时容易搞混。
实验14的单选按钮改背景色,本质上是 StringVar 和回调函数的配合。v.set(2) 让第三个单选按钮成为默认选中项,v.get() 取到的是 value 参数,不是按钮文本,这是 tkinter 里最容易弄混的地方。我在实际项目中更常用 ttk.Radiobutton,外观更现代,但逻辑一模一样。
实验15的猜数字参考程序里有几处 config 写法不太规范,比如 lbl_min.config(lbl_min, text=nmin) 这种调用,第一个位置参数会被忽略,正确写法是 lbl_min.config(text=nmin)。这类代码能跑,是因为 config 只按关键字参数生效,位置参数被静默丢弃。拿到手后我第一件事就是把所有 config 调用统一成纯关键字参数形式,既清晰又能避免未来 tkinter 版本收紧参数检查时报错。
from tkinter import ttk # 用 ttk 替换原始 Button,外观更现代 btnG = ttk.Button(frm3, text="开 始") btnG.bind("<Button-1>", btnGuess)改造后的逻辑主线不变:随机数在程序启动时生成,用户输入后通过比较更新区间上下界,同时维护猜测次数。二分检索在这里的体现是,每次猜测都能把区间缩小一半,最坏情况 10 次内必定猜中 1 到 1000 之间的数。如果你要把游戏改成更难的模式,可以把 random.randint 的范围加大,或者把边界更新逻辑改成完全不提示高低,这种改法能帮自己理解二分检索的收敛速度。
最后给你一个验证技巧:拿到这份文档后,不需要开完整 IDE,用命令行 python -i 进入交互模式,把题目代码分段粘进去执行。文件操作类的题跑完检查当前目录下是否生成了 a.dat 或 sample_copy.txt;GUI 类的题跑完手动点击每个按钮,确认回调函数生效。我自己当初做实验7元组那道题时也报了一模一样的 AttributeError,后来养成一个习惯:拿到一段代码先看容器类型,再决定能不能 append、能不能做下标赋值。从那以后我每次跑通一段代码都顺手把关键变量的类型和值 print 出来,毕竟类型认错才是 Python 新手最大的暗坑。希望帮到你。
本文还有配套的精品资源,点击获取