Python函数编程核心:作用域、递归与算法实战解析

发布时间:2026/10/24 12:49:22
Python函数编程核心:作用域、递归与算法实战解析

简介:本资源为《Python编程实践》第8章练习题及解答的PDF文档,作者陈波、刘慧君,面向正在学习Python基础语法与函数式编程的初学者及高校学生,帮助其通过配套习题巩固课堂知识、检验掌握程度。压缩包内仅含1个PDF文件,大小约224KB,轻量便携,适合在电脑或移动设备上随时查阅。内容覆盖函数编程、递归、循环、字符串处理、数字转换、回文素数等多个主题,具体包括整数各位数字之和、递归求和、十进制转十六进制/八进制/二进制、不特定多参数乘积、前100个回文素数打印以及折半查找等典型题目。每道题均给出答案与思路解析,并附示例代码,便于读者对照理解函数参数传递、递归调用、取模运算与素数筛选等关键知识点。目前已有1935人学习下载,可作为课后自测与期末复习的实用参考。

1. 变量作用域到底是怎么回事:从一个“同名不同命”的困惑说起

很多人第一次学 Python 函数时,都遇到过这样的怪事:函数里面写了一个x = 77,函数外面也有一个x = 1,调用完函数再打印外面的x,它居然还是 1。明明函数里改了呀?这就像你在自己家冰箱里放了一瓶可乐,邻居家冰箱里也有一瓶,你打开邻居家冰箱把可乐换成了雪碧,回到自己家打开冰箱——可乐还在。两瓶可乐同名,但住在不同的“作用域”里,互不干扰。

这个“同名不同命”的现象,就是 Python 变量作用域最直观的体现。搞懂它,你才能明白为什么有些变量函数里能用、外面不能用,为什么有些函数改完值外面却“看不见”。而这份《Python编程实践》第 8 章的练习题及解答,恰好把作用域、递归、进制转换、回文素数、折半查找这些函数编程的核心概念串成了一条线。它不是干巴巴地讲语法,而是用一道道题逼你把“函数到底怎么跑”想清楚。接下来,我们就把这些概念一个个拆开,用大白话讲透。

2. 函数编程的底层逻辑:为什么变量会“认生”

2.1 局部变量与全局变量:函数里的“优先权”规则

先看一个典型场景。假设有这样一个程序:

x = 1 y = 1 def func(): x = 77 z = y * 2 print("x+y=", x + z) func() print("x=", x) print("y=", y)

运行结果是x+y= 79,然后x= 1,y= 1。为什么函数里的x变成了 77,外面的x还是 1?因为 Python 在函数内部遇到赋值语句x = 77时,会认为x是一个局部变量,它只在函数执行期间存在,函数一结束就消失了。而y在函数里没有被赋值,Python 就会去全局作用域找,找到了外面的y = 1,所以z = 1 * 2 = 2,最终x + z = 77 + 2 = 79。

通俗地说,函数就像一个房间,里面可以放自己的东西(局部变量),也可以借用外面的东西(全局变量),但如果你在房间里放了一个和外面同名的东西,房间里的人就只认自己那个,外面的完全不受影响。这就是“局部优先”原则。

注意:如果你在函数里只是想读取全局变量,不赋值,那没问题;一旦赋值,Python 就默认你要创建一个新的局部变量,除非你用global声明。

2.2 递归:函数自己调用自己的“套娃”艺术

第 8 章里有一道题要求计算f(i) = 1/2 + 2/3 + 3/4 + … + i/(i+1),还有一道题用递归求1 到 9 的奇数和。递归听起来玄乎,其实就是函数自己调用自己,把一个大问题拆成一个小一号的同类问题,直到拆到最简单的情况直接给出答案。

比如求奇数和:

def func(x): if x <= 1: return 1 return x + func(x - 2) print(func(9))

执行过程是这样的:func(9)发现 9 大于 1,于是返回9 + func(7);func(7)又返回7 + func(5)……一路拆到func(1),它直接返回 1。然后一层层往回加:1 + 3 + 5 + 7 + 9 = 25。这就像你有一摞盘子,每次拿掉两个,直到只剩一个,再把拿掉的盘子数加起来。

递归的关键在于终止条件。没有if x <= 1: return 1这一句,函数就会无限调用自己,直到程序崩溃。所以写递归时,第一件事就是想清楚:什么时候停下来?

2.3 不定参数:一个函数怎么吃下“不特定多个数”

第 8 章还有一道题要求“计算一个或不特定多个数的乘积”。Python 用*temp这种写法来接收任意数量的参数:

def caculate(*temp): ans = 1 for x in temp: ans *= x return ans print(caculate(1, 2, 3, 4, 5))

这里的*temp会把传入的所有参数打包成一个元组,函数内部就可以像遍历列表一样遍历它。你传 3 个数,temp就是(1, 2, 3);传 5 个数,就是(1, 2, 3, 4, 5)。这就像你去自助餐厅,拿一个盘子,想装几样菜就装几样,厨师(函数)只管把你盘子里的菜全炒了。

这种写法在实际开发中很常见,比如日志函数要接收任意数量的消息片段,或者数学计算函数要接收任意多个操作数。它让函数的接口变得灵活,不用提前规定“你必须传 3 个参数”。

3. 数字转换与回文素数:两个经典练习背后的思维训练

3.1 进制转换:为什么“除 R 取余、反序输出”能行

第 8 章要求写三个函数,分别把整数转成十六进制、八进制、二进制字符串。核心方法就一句话:不断用 n 除以 R,取余数,直到 n 为 0,最后把余数反着拼起来。

以十进制转二进制为例,假设 n = 13:

  • 13 % 2 = 1,n 变成 6
  • 6 % 2 = 0,n 变成 3
  • 3 % 2 = 1,n 变成 1
  • 1 % 2 = 1,n 变成 0

余数依次是 1、0、1、1,反序就是 1101,正好是 13 的二进制。为什么反序?因为第一次算出来的是最低位,最后算出来的是最高位,就像你读数字是从左往右读,但计算时是从右往左拆的。

十六进制稍微麻烦一点,因为 10 到 15 要用 A 到 F 表示。代码里用一个字典做映射:

Map = {0:"0", 1:"1", ..., 10:"A", 11:"B", 12:"C", 13:"D", 14:"E", 15:"F"}

每次取余后查字典,最后同样反序。这个思路不仅适用于 2、8、16 进制,换成任意进制 R 都成立,只要处理好大于 9 的数字怎么表示就行。

3.2 回文素数:两个判断条件怎么优雅地合在一起

回文素数是指一个数既是素数又是回文数,比如 131。第 8 章要求打印前 100 个回文素数,每行 10 个。这道题把两个独立的知识点揉在了一起:素数判断和回文判断。

素数判断用的是筛选法:从 2 开始,把每个素数的倍数都标记为“非素数”,剩下的就是素数。代码里用了一个字典count来记录被筛掉的数:

prime = [] count = {} for i in range(2, 100000): temp = i if temp not in count: prime.append(temp) while temp <= 100000: count[temp] = 1 temp += i

回文判断则是把数字转成字符串,然后比较首尾字符是否对称:

def judge(temp): value_str = str(temp) Len = len(value_str) for i in range(Len // 2): if value_str[i] != value_str[Len - 1 - i]: return False return True

最后遍历素数列表,用judge过滤出回文数,凑够 100 个就停。这道题的价值在于:它让你练习了把两个小功能组合成一个大功能,而且每个小功能都写成独立的函数,逻辑清晰,改起来也方便。

3.3 折半查找:为什么“砍一半”能这么快

折半查找(也叫二分查找)是第 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 "没有找到"

为什么快?因为每次比较都能把查找范围缩小一半。20 个学生,最多比较 5 次就能确定有没有;1000 个学生,最多 10 次。这就像你猜一个 1 到 100 之间的数字,每次猜中间值,最多 7 次就能猜中。折半查找的前提是数据必须有序,如果列表是乱序的,这个方法就失效了。

4. 常见误区与易混概念:这些坑你踩过几个

4.1 局部变量和全局变量同名时,到底用哪个

易混点:函数里写x = 77,外面也有x = 1,函数里打印x是 77,外面打印还是 1。很多人以为函数里的赋值会“覆盖”外面的。

为什么混:因为其他语言(比如 C)里,函数内外的同名变量可能通过指针或引用互相影响,但 Python 的赋值是“创建新名字”,不是“修改原名字”。

怎么区分:记住一句话——函数内赋值即局部,除非用global声明。如果你真的想在函数里修改全局变量,得写global x,然后x = 77,这样外面的x才会变。

4.2 递归和循环,什么时候非得用递归

易混点:求奇数和既可以用递归,也可以用循环,那为什么还要学递归?

为什么混:因为很多问题用循环也能解决,初学者觉得递归多此一举。

怎么区分:递归适合问题本身是嵌套结构的场景,比如“分治法找假硬币”——把一堆硬币分成两半,称一称,假币在哪边就继续分哪边。这种“大问题拆成同类小问题”的逻辑,用递归写出来几乎和自然语言一样直白。循环适合“重复做同一件事直到满足条件”,比如遍历列表、累加求和。简单说:结构嵌套用递归,线性重复用循环。

4.3 进制转换时,为什么余数要反序

易混点:算出来的余数直接拼起来不行吗?为什么非要反序?

为什么混:因为第一次算出的余数是最低位,但人读数字是从最高位读的。

怎么区分:拿 13 转二进制举例,余数依次是 1、0、1、1,如果直接拼是 1011,但 1011 是 11,不是 13。反序后 1101 才是 13。记住:先算出来的是低位,后算出来的是高位,输出时要倒过来。

4.4 折半查找为什么要求数据有序

易混点:折半查找看起来只是“每次取中间”,跟数据有没有排序有什么关系?

为什么混:因为代码里只写了比较中间值,没写排序,容易忽略前提。

怎么区分:折半查找的逻辑是“目标比中间小就去左边,比中间大就去右边”,这个判断成立的前提是左边所有数都比中间小,右边所有数都比中间大。如果数据没排序,左边可能藏着比中间大的数,你的判断就错了。所以用折半查找之前,必须先排序。

4.5 不定参数*temp和普通参数混用时,顺序怎么排

易混点:def func(a, *temp)和def func(*temp, a)有什么区别?

为什么混:因为*temp看起来像个普通参数,实际上它会把后面所有位置参数都吃掉。

怎么区分:*temp必须放在普通位置参数后面。def func(a, *temp)表示第一个参数给a,剩下的全给temp;def func(*temp, a)在 Python 3 里表示a必须用关键字传参,比如func(1, 2, a=3)。初学阶段记住:普通参数在前,*temp在后,就不会出错。

5. 什么时候该翻出这份练习题:一个“卡住时”的用法示例

学 Python 函数编程,最怕的不是看不懂语法,而是看懂了却写不出来。这份第 8 章练习题及解答的价值,恰恰在于它把“写不出来”的瞬间提前暴露给你。比如你觉得自己会递归了,但让你写一个“用递归求 1 到 9 的奇数和”,你可能卡在终止条件上;你觉得自己会进制转换了,但让你处理十六进制的 A 到 F,你可能忘了做映射。

那什么时候该用它?我的经验是:当你学完函数、递归、循环这几章,感觉自己“好像会了”的时候,直接翻到这份题,盖住答案,自己先写一遍。写不出来,说明你只是“看懂了”,没真正掌握;写出来了但和答案不一样,对比一下思路,往往能发现更简洁的写法。比如回文素数那道题,答案用了筛选法先把素数全找出来,再过滤回文数,比“逐个判断是不是素数、是不是回文”效率高得多。这种“先批量筛选、再条件过滤”的思路,在以后处理大数据时非常有用。

但也要注意,这份题不是万能的。它偏重函数编程的基础训练,不涉及面向对象、文件操作、异常处理这些后续章节的内容。如果你已经能熟练写出递归、进制转换、折半查找,那这份题对你来说就偏简单了,可以直接跳过,去练更复杂的项目。另外,题目里的代码风格偏“教学版”,比如变量名用temp、ans,实际开发中建议用更有意义的名字,比如remainder、total。

提示:做题时不要只看答案的“思路”部分,一定要自己动手敲一遍。很多坑(比如递归忘记写终止条件、折半查找忘记排序)只有亲手写才会踩到。

从那以后我每次带人学 Python 函数,都强制让他们先自己做一遍第 8 章的题,做完再对照答案。因为“看会”和“写会”之间,隔着一条河,而练习题就是那座桥。希望这份资料能帮你顺利过河。

本文还有配套的精品资源,点击获取