Python第8章练习精讲:函数作用域、递归与迭代、进制转换等核心基本功

发布时间:2026/10/28 3:17:28
Python第8章练习精讲:函数作用域、递归与迭代、进制转换等核心基本功 简介这份资源是《Python编程实践》第8章练习题及解答作者为陈波、刘慧君面向正在学习Python基础语法与函数式编程的初学者及高校学生帮助其通过课后练习巩固函数定义、递归、循环与字符串处理等核心技能。压缩包内共1个PDF文件约224KB集中呈现第8章各练习题的题目、参考答案与解题思路。内容覆盖多个典型题型计算整数各位数字之和、用递归求f(i)1/22/3…i/(i1)、将整数转换为十六进制/八进制/二进制字符串、计算不特定多个数的乘积以及打印前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拿到当前最低位数字累加进anstemp // 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(xy, x z) x 1 # 这行在 print 之后不影响上面的 x x 1 y 1 func() print(x, x) # 全局 x 仍是 1 print(y, y) # 全局 y 仍是 1输出是xy 79、x 1、y 1。关键在于func内部的x 77创建了一个局部变量和全局x同名但互不影响y在函数内没有赋值所以按 LEGB 规则去全局找拿到 1z 1 * 2 2x z 77 2 79。函数返回后全局的x、y都没被动过。这道题的价值在于让你亲手确认一件事函数内只要出现赋值语句这个名字在整个函数体内就是局部的哪怕赋值写在后面。我见过不少人在函数里先读后写同一个名字结果报UnboundLocalError根子就在这里。3.2 递归与迭代的对照8-4 级数求和与 8-9 找假币8-4 的参考解给的是递归版奇数和但题目本身要的是f(i)1/22/3…i/(i1)资料里两段代码其实是两个不同的东西这点后面避坑会提。先看递归展开的思路def func(x): if x 1: return 1 return x func(x - 2) # 每次减 2只累加奇数 print(func(9)) # 97531 25递归的终止条件是x 1每次递归把问题规模减 2展开过程就是func(9)9func(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/22/3…i/(i1)但资料里给的递归代码算的是1~9的奇数和输出 25。原因资料在整理时把两道不同题的代码混在了一起级数求和的正确写法其实是 8-4 后半段那个while循环版本。解决以题目描述为准级数求和用ans x / (x 1)的循环版递归那段只当递归展开的教学例子看别当成这题的答案。4.3 进制转换对 0 和负数返回空串现象输入 0My_Binary(0)返回而不是0输入负数while temp直接不进入同样返回空串。原因循环条件是while temp0 和负数都不满足取模对负数还会得到负余数。解决函数开头加if temp 0: return 0负数先取绝对值再转换、最后补负号或者干脆在入口处断言输入为正整数。4.4 折半查找里 int 转换和字符串比较混用现象学号列表里存的是字符串比较时一会儿int(Stu[mid][0])一会儿直接比字符串结果顺序错乱、查找失败。原因字符串比较是字典序20189X和201822按字符逐位比和数值大小不是一回事。解决统一在查找前把学号转成同一类型要么全转int前提是学号纯数字要么全按字符串比较并保证列表也按字符串序排好。资料里Stu用str(2018i)生成是纯数字字符串转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 0while num直接跳过函数返回None调用方以为“没有假币”其实是输入非法。从那以后我每次写这类区间收缩的函数都在入口强制走一遍参数校验——if not coins or n 0: raise ValueError宁可早报错也不让非法输入悄悄变成合法返回值。这份第 8 章的练习看着是给学生对答案用的但把每道题的边界和返回值想清楚就是一次小型的工程习惯训练。希望帮到你。本文还有配套的精品资源点击获取