Python第8章练习精讲:函数作用域、递归与迭代、进制转换等核心基本功
简介:这份资源是《Python编程实践》第8章练习题及解答,作者为陈波、刘慧君,面向正在学习Python基础语法与函数式编程的初学者及高校学生,帮助其通过课后练习巩固函数定义、递归、循环与字符串处理等核心技能。压缩包内共1个PDF文件,约224KB,集中呈现第8章各练习题的题目、参考答案与解题思路。内容覆盖多个典型题型:计算整数各位数字之和、用递归求f(i)=1/2+2/3+…+i/(i+1)、将整数转换为十六进制/八进制/二进制字符串、计算不特定多个数的乘积,以及打印前100个回文素数等,并配有代码示例与思路说明。已有1935人学习,适合作为课后自测与查漏补缺的练习材料,也可供教师参考出题与讲解。
1. 从一份“答案纸”说起:这套第 8 章练习到底能拿来干什么
很多人拿到一份练习题解答,第一反应是“对答案”。但这份《Python 编程实践》第 8 章的资料,真正有价值的不是那几行输出结果,而是每道题后面附的“思路”二字。它把函数作用域、递归展开、进制转换、素数筛选、折半查找、分治找假币这些后端开发里反复出现的基本功,压缩进了九道题里。你如果是刚学完函数、想找一组能跑、能改、能验证的小项目练手,这份资料就是现成的骨架;如果你已经写过一阵子业务代码,拿它当一次“基础回炉”也合适——尤其是 8-7 的素数筛选和 8-9 的递归改迭代,能看出一个人对循环边界和状态维护的敏感度。下面我按“先跑通、再拆结构、最后改”的顺序,把它当成一个小工程来拆。
2. 环境与目录:把散落的代码块拼成一个可运行的小工程
2.1 运行环境与依赖
这份资料本身没有第三方依赖,纯标准库就能跑。我一般会固定一个 Python 3.8 以上的解释器,因为代码里用到了//整除、*temp可变参数、[::-1]切片反转这些特性,3.x 全系都支持,但 3.6 以下对 f-string 之外的写法兼容性没必要去赌。虚拟环境不是必须的,但如果你打算把九道题拆成多个文件反复改,建一个干净的环境能避免和系统里其他包打架。
# 建一个独立环境,避免和已有项目混在一起 python3 -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate python --version # 确认在 3.8 以上这里没有requirements.txt,因为确实不需要。所有代码只依赖内置的input、print、range、sum、len这些。参数上唯一要注意的是:8-3、8-4、8-5 这几道题都用input()读整数,跑的时候要手动输入,不适合直接丢进自动化脚本里批量跑——后面改造章节我会讲怎么把它们改成可传参的函数。
2.2 建议的目录结构
原始资料是一段段代码块,直接复制到一个文件里会互相覆盖变量名(比如caculate被 8-3、8-4 重复定义)。我习惯按题号拆文件,再留一个统一入口:
chapter8/ ├── p8_1_scope.py # 作用域与全局变量 ├── p8_2_scope_trace.py # 同名变量执行过程 ├── p8_3_digit_sum.py # 各位数字之和 ├── p8_4_series.py # 分数级数求和 ├── p8_5_base_convert.py # 十六/八/二进制转换 ├── p8_6_product.py # 可变参数求积 ├── p8_7_palindrome_prime.py # 回文素数 ├── p8_8_binary_search.py # 折半查找 ├── p8_9_false_coin.py # 分治找假币(迭代版) └── run_all.py # 可选:统一调用无输入依赖的函数这样拆的好处是每道题的作用域互不干扰,改一道不会把另一道跑挂。run_all.py只调用那些不依赖input()的函数,方便做回归验证。
2.3 先跑通最小闭环
拿 8-3 的各位数字之和做第一次验证,因为它逻辑最短、最容易看出环境有没有问题:
def caculate(temp): ans = 0 while temp: # temp 为 0 时退出,等价于 while temp != 0 ans += temp % 10 # 取出最低位 temp //= 10 # 消去最低位 return ans a = int(input("请输入一个整数:")) print(caculate(a))逻辑说明:temp % 10拿到当前最低位数字,累加进ans;temp //= 10把最低位砍掉。循环条件是while temp,当数字被除到 0 时自然结束。参数上唯一要小心的是负数——-123 % 10在 Python 里是 7 而不是 -3,所以这段代码对负数的结果不符合“各位数字之和”的直觉,后面避坑章节会专门讲。跑通这一步,说明解释器和输入输出都正常,剩下的题可以逐个搬进来。
3. 核心模块拆解:九道题背后的五类基本功
3.1 作用域与全局变量:8-1、8-2 的执行过程
8-2 那段代码是整份资料里最容易被“看走眼”的:
def func(): x = 77 z = y * 2 # y 不在 func 内定义,向上找全局 y print("x+y=", x + z) x = 1 # 这行在 print 之后,不影响上面的 x x = 1 y = 1 func() print("x=", x) # 全局 x 仍是 1 print("y=", y) # 全局 y 仍是 1输出是x+y= 79、x= 1、y= 1。关键在于:func内部的x = 77创建了一个局部变量,和全局x同名但互不影响;y在函数内没有赋值,所以按 LEGB 规则去全局找,拿到 1,z = 1 * 2 = 2,x + z = 77 + 2 = 79。函数返回后,全局的x、y都没被动过。这道题的价值在于让你亲手确认一件事:函数内只要出现赋值语句,这个名字在整个函数体内就是局部的,哪怕赋值写在后面。我见过不少人在函数里先读后写同一个名字,结果报UnboundLocalError,根子就在这里。
3.2 递归与迭代的对照:8-4 级数求和与 8-9 找假币
8-4 的参考解给的是递归版奇数和,但题目本身要的是f(i)=1/2+2/3+…+i/(i+1),资料里两段代码其实是两个不同的东西,这点后面避坑会提。先看递归展开的思路:
def func(x): if x <= 1: return 1 return x + func(x - 2) # 每次减 2,只累加奇数 print(func(9)) # 9+7+5+3+1 = 25递归的终止条件是x <= 1,每次递归把问题规模减 2,展开过程就是func(9)=9+func(7)=…=25。这种“规模递减 + 边界返回”的写法适合教学,但真实项目里递归深度一上去就有栈溢出风险。8-9 正好给了反面示范——把分治找假币的递归改成迭代:
def findFalseCoin(coins, start, n): bottom = start num = n while num: mid = num // 2 left = sum(coins[bottom:bottom + mid]) right = sum(coins[bottom + mid:bottom + mid + mid]) if left < right: pass # 假币在左半,bottom 不动 elif left > right: bottom = bottom + mid # 假币在右半,起点右移 elif mid * 2 < num: return bottom + num - 1 # 奇数个且两边相等,剩下那个是假币 else: return None # 偶数个且两边相等,没有假币 num = mid if num == 1: return bottom逻辑说明:用while num替代递归调用,bottom和num就是原来递归栈里保存的两个状态。每次比较左右两堆重量,左轻则范围不变、规模减半;右轻则起点右移、规模减半;两边相等时根据奇偶判断剩余那枚或直接返回None。参数start是当前考察区间的起点,n是区间长度,这两个量在迭代版里必须显式维护,这正是递归改迭代的核心——把隐式的调用栈摊平成显式变量。
3.3 进制转换:8-5 的取模反序套路
8-5 要求写三个函数分别转十六、八、二进制。三个函数结构完全一样,只是除数和字符映射不同:
def My_Hexadecimal(temp): ans = "" Map = {0:"0",1:"1",2:"2",3:"3",4:"4",5:"5",6:"6",7:"7", 8:"8",9:"9",10:"A",11:"B",12:"C",13:"D",14:"E",15:"F"} while temp: ans += Map[temp % 16] # 取余得到当前位 temp //= 16 # 整除消去当前位 return ans[::-1] # 取模是从低位到高位,最后要反转 def My_Octonary(temp): ans = "" while temp: ans += str(temp % 8) temp //= 8 return ans[::-1] def My_Binary(temp): ans = "" while temp: ans += str(temp % 2) temp //= 2 return ans[::-1]参数说明:temp是待转换的十进制整数。核心公式是a = n % R; n //= R,直到n为 0,得到的余数序列是低位在前,所以最后[::-1]反转。十六进制多了一步字典映射,把 10 到 15 换成 A 到 F。这里有个隐含前提:输入是正整数。temp为 0 时三个函数都返回空串,而不是"0",这是资料里没处理干净的边界,改造章节会给修法。
3.4 可变参数与素数筛选:8-6、8-7 的两个典型模式
8-6 用*temp接收不特定多个数,是 Python 可变参数的入门写法:
def caculate(*temp): ans = 1 for x in temp: # temp 是一个元组 ans *= x return ans print(caculate(1, 2, 3, 4, 5)) # 120*temp把调用时传入的所有位置参数打包成元组,函数体内按普通可迭代对象遍历即可。注意它不能直接接收列表,传caculate([1,2,3])会把整个列表当成一个元素,乘出来是列表本身——这是可变参数最常见的误用。
8-7 的回文素数用的是素数筛选法,思路比逐个判断更值得看:
def Prime(): prime = [] count = {} for i in range(2, 100000): temp = i if temp not in count: # 没被标记过,说明是素数 prime.append(temp) while temp <= 100000: count[temp] = 1 # 把 i 的倍数全部标记为合数 temp += i return prime def judge(temp): s = str(temp) n = len(s) for i in range(n // 2): if s[i] != s[n - 1 - i]: return False return TruePrime()用字典count当标记数组,外层遍历到i时如果没被标记就收进prime,然后把i的所有倍数标掉。judge()用双指针从两端向中间比对字符串,判断回文。主流程遍历素数列表,满足回文就收进ans,满 100 个就break,最后按每行 10 个输出。这套“先筛后判”的结构在需要批量判断素数的场景里比逐个试除快一个量级,代价是内存占用随上限线性增长。
3.5 折半查找:8-8 的边界处理
8-8 在一个按学号递增排好序的二维列表里做折半查找:
def Binary_Find(Stu): bottom = 0 top = len(Stu) - 1 while bottom <= top: mid = (bottom + top) // 2 if num == int(Stu[mid][0]): return "已找到" elif num > int(Stu[mid][0]): bottom = mid + 1 # 目标在右半 elif num < int(Stu[mid][0]): top = mid - 1 # 目标在左半 return "没有找到"参数说明:Stu是二维列表,每个子列表第一个元素是学号字符串;num是全局变量,来自input()。循环条件是bottom <= top,等号不能丢,否则区间只剩一个元素时会漏判。mid用//取整,bottom = mid + 1和top = mid - 1保证区间严格收缩,不会死循环。资料里这段代码有个小瑕疵:return "已找到"后面跟了break,但return已经退出函数,break是死代码;另外else分支挂在while上而不是if上,写法上容易让人误读,改造时值得顺手清理。
4. 避坑与排查:跑这份练习时最容易翻车的五处
4.1 函数内先读后写同名变量报 UnboundLocalError
现象:在函数里写了print(x)之后又写x = 1,运行直接抛UnboundLocalError: local variable 'x' referenced before assignment。原因:Python 在编译期就确定,函数体内只要出现对某名字的赋值,这个名字整个函数内都是局部的,print(x)时局部x还没赋值。解决:要么把赋值挪到读取之前,要么在函数开头显式global x(但更推荐改参数传递,少用全局)。
4.2 8-4 的题目和参考解对不上
现象:题目要的是1/2+2/3+…+i/(i+1),但资料里给的递归代码算的是1~9的奇数和,输出 25。原因:资料在整理时把两道不同题的代码混在了一起,级数求和的正确写法其实是 8-4 后半段那个while循环版本。解决:以题目描述为准,级数求和用ans += x / (x + 1)的循环版;递归那段只当递归展开的教学例子看,别当成这题的答案。
4.3 进制转换对 0 和负数返回空串
现象:输入 0,My_Binary(0)返回""而不是"0";输入负数,while temp直接不进入,同样返回空串。原因:循环条件是while temp,0 和负数都不满足;取模对负数还会得到负余数。解决:函数开头加if temp == 0: return "0",负数先取绝对值再转换、最后补负号,或者干脆在入口处断言输入为正整数。
4.4 折半查找里 int 转换和字符串比较混用
现象:学号列表里存的是字符串,比较时一会儿int(Stu[mid][0])一会儿直接比字符串,结果顺序错乱、查找失败。原因:字符串比较是字典序,"20189X"和"201822"按字符逐位比,和数值大小不是一回事。解决:统一在查找前把学号转成同一类型,要么全转int(前提是学号纯数字),要么全按字符串比较并保证列表也按字符串序排好。资料里Stu用str(2018+i)生成,是纯数字字符串,转int比较没问题,但真实数据里混入字母就会翻车。
4.5 素数筛选的上限写死导致结果不足
现象:把range(2, 100000)的上限调小,比如改成 1000,跑出来回文素数不到 100 个,程序却正常结束不报错。原因:筛选上限决定了候选池大小,前 100 个回文素数里最大的那个可能超过上限,池子不够自然凑不齐。解决:上限要留足余量,或者改成动态扩展——先筛一段,不够再翻倍重筛。我一般会先估算第 100 个回文素数的量级,再定上限,而不是拍脑袋写个 100000。
5. 二次开发:把练习代码改成能复用的小工具
这份资料最大的改造空间,是把一堆依赖input()的脚本变成可传参、可测试的函数库。以 8-5 的进制转换为例,原始三个函数各写一遍循环,重复度高,我一般会抽成一个通用函数:
def to_base(n, base): """把非负整数 n 转成 base 进制的字符串,base 取 2~16""" if n == 0: return "0" digits = "0123456789ABCDEF" ans = "" while n: ans = digits[n % base] + ans # 直接头插,省掉最后反转 n //= base return ans # 验证:和内置函数对拍 for v in [0, 1, 7, 255, 4095]: assert to_base(v, 16) == format(v, "X") assert to_base(v, 8) == format(v, "o") assert to_base(v, 2) == format(v, "b")改造点有三个:一是用digits字符串下标代替字典,省掉映射表;二是ans = digits[n % base] + ans头插,免去最后的[::-1];三是加了n == 0的边界。验证方法很直接——拿内置的format(v, "X")做对拍,几个边界值一跑就知道对不对。这种“自己写一遍再和标准库对拍”的习惯,比单纯看答案更能暴露边界问题。
8-9 的找假币也值得再推一步。原始迭代版只返回假币下标,但实际场景里往往还要知道假币是偏轻还是偏重。可以在每次比较时记录方向:
| 改造点 | 原始写法 | 改造后 |
|---|---|---|
| 返回值 | 仅下标或 None | 返回(下标, "轻"/"重") |
| 边界 | num == 1直接返回 | 区分“只剩一枚且已知偏轻/偏重” |
| 输入校验 | 无 | 校验硬币数 ≥ 1、重量为正 |
表格里第三行是我踩过的坑:有次传了个空列表进去,num = 0,while num直接跳过,函数返回None,调用方以为“没有假币”,其实是输入非法。从那以后我每次写这类区间收缩的函数,都在入口强制走一遍参数校验——if not coins or n <= 0: raise ValueError,宁可早报错,也不让非法输入悄悄变成合法返回值。这份第 8 章的练习看着是给学生对答案用的,但把每道题的边界和返回值想清楚,就是一次小型的工程习惯训练。希望帮到你。
本文还有配套的精品资源,点击获取