☰
Python函数练习题:从参数传递到递归的进阶指南
2026/10/17 1:25:08 网站建设 项目流程

1. 函数练习题的真正意义:没写够量,别谈掌握

Python 函数这块儿,语法上看起来就三件事:def定义、参数传递、return返回。很多人学到这里觉得“哦,懂了”,然后一写代码就卡壳——要么函数写完不干活,要么变量串台,要么返回了None还死活找不到原因。这不是个别现象,是“只看了语法没做够题”的典型症状。

函数定义与使用练习题,本质上就是一个把知识点从“眼睛会了”逼到“手也会了”的必经环节。它不适合零基础完全没听过语法的人,但非常适合刚学完概念、想在动手写代码时把函数用顺手的新手。哪怕你已经写了几个月脚本,回头把这些练习题过一遍,多半也能发现几个自己平时没意识到的坏习惯。

我见过的初学者里,函数这块最容易出问题的,其实不是语法本身,而是“思路没有模块化”。所谓“函数”,说白了一点——把一段反复要用的逻辑打包成盒子,给它一个名字,以后喊名字就能执行。练习题的价值,就是用各种场景逼你习惯这种“打包”的思维。这个思维能力,只有靠量变堆出来。

另外还有一层价值:很多练习题看起来是在考“定义函数”,实际上是在考你“拆问题的能力”。一个求和函数怎么写,一个判断质数的函数怎么写,一个字符串反转的函数怎么写——每个题都在训练你把大问题拆成子问题的直觉。这种能力一旦有了,后面学装饰器、闭包、递归、类方法,全都一通百通。

2. 练习题设计的底层逻辑:三大核心知识点是反复围绕的焦点

函数功能够用到什么程度,判断标准其实很朴素:能不能把重复的代码收敛到一个可复用的函数里,并且调用得干净、优雅。围绕这个标准,练习题的套路无非就是在三个核心知识点上反复变着花样设计:参数怎么传、返回怎么用、作用域怎么避坑。

2.1 参数传递:位置参数、默认参数、关键字参数的本质区别

参数这关如果没想明白,后面看别人写代码基本是一头雾水。有人写def foo(a, b=2)有人写def bar(*args, **kwargs),你会觉得都是“传参”为什么长得不一样?其实底层逻辑就这么几条:

  • 位置参数:调用时按顺序填入,最简单也最容易漏参。
  • 默认参数:调用时不给值也能跑,但有个经典坑——默认参数如果是可变对象(比如列表、字典),很容易出现记忆留痕。
  • 关键字参数:调用时用key=value形式,顺序可以打乱,可读性最好。
  • *args和**kwargs:用来接收任意数量的位置参数和关键字参数,常用于写通用接口。

练习题里为啥反复设置参数的题目?因为函数调用的灵活度全看参数设计。同一个函数,支持add(1, 2)也支持add(1, 2, 3, 4),这背后靠的就是变长参数。很多新手一上来就背*args语法,却不理解它在“参数打包与解包”中的本质,结果一遇到嵌套调用就懵。

下面给一道非常典型的参数综合练习题,几乎概括了以上所有要点:

# 习题1:实现一个带默认参数、可变参数的批量求和函数 def flex_sum(first, *nums, start=0, **options): """ first: 必传的第一个数字 nums: 任意数量的后续数字 start: 起始值,默认0 options: 是否打印详细过程用 """ total = start values = (first,) + nums for v in values: total += v if options.get("verbose"): print(f"求和过程: {' + '.join(map(str, values))} = {total}") return total print(flex_sum(1, 2, 3)) # 6 print(flex_sum(1, 2, 3, 4, 5, start=100)) # 115 print(flex_sum(10, 20, verbose=True)) # 30 并打印过程

这道题把位置参数、*args、默认参数、**kwargs全揉在了一起。你能不看答案写出来,说明参数这块基本过关了。

2.2 返回值:return和print的边界,以及多值返回的实现方式

函数到底要不要return?很多新手写函数喜欢在里面print结果,然后说“为什么拿不到值”。这就是没分清print是“输出到控制台”,return是“把值交给调用方”。练习题里专门设计返回值题目,就是让你掉一次这个坑、记一辈子。

多值返回在 Python 里其实是用元组实现的。看起来是return a, b,本质是return (a, b)。这在解包赋值时特别爽,但要注意别在多层调用中把元组当单值用,很容易越解越乱。

# 习题2:返回商和余数,并统计除数是否为0 def divide_with_remainder(dividend, divisor): if divisor == 0: return None quotient = dividend // divisor remainder = dividend % divisor return quotient, remainder # 实际是返回元组 result = divide_with_remainder(17, 5) print(result) # (3, 2) q, r = result # 元组解包 print(q, r) # 3 2

这里有个细节值得说出来:设计函数时,如果遇到异常情况(比如除数为0),到底是返回None、抛异常还是返回一个特殊标记?练习题的意义就在这里——它不是让你背标准答案,是逼你想清楚每一种返回方式的适用场景。返回None简单直接,但调用方容易忽略;抛异常更严谨,但调用方必须处理;返回特殊标记则带着约定性。这道题我故意用None,就是提醒你注意这个边界。

2.3 变量作用域:局部变量、全局变量与global的滥用的风险

作用域问题,在练习题里往往以“猜输出”的形式出现。为什么?因为变量的可见范围直接决定了函数的“隔离性”。函数内部修改外部变量,必须用global声明,这点几乎人人都知道,但真的要在代码里做决策时,很多人会过度使用global,导致函数变成“打赢了就跑,留下一堆全局变量”的祸害。

# 习题3:猜输出,理解局部变量与全局变量 count = 0 def demo_a(): count = 10 # 局部变量,与全局无关 return count def demo_b(): global count count = count + 1 return count print(demo_a()) # 10 print(count) # 0,全局没变 print(demo_b()) # 1,全局count变了 print(count) # 1

你发现没有,demo_a里count = 10只是创建了一个局部变量,并没有修改全局的count,只有demo_b通过global声明才能影响到全局。练习题反复设置这类题目,是要你形成一种条件反射——看到“在函数里修改外部变量”,第一时间就要去查有没有global声明。

注意:能在函数内通过参数传递实现逻辑,就不要动global。全局变量越多,函数的可复用性越差,多线程环境下还容易引入竞态问题。练习题阶段养成的习惯,会直接影响你上班后写业务代码的质量。

3. 练习题的梯度设计:从语法复现到算法思维,逐步加码

函数练习题如果只是“把代码改成函数”这种重复劳动,练着练着就会犯困。真正让我觉得有价值的练习题,是按难度梯度设计的:先是语法层面复现,然后到逻辑层面应用,最后到算法思维层面的综合运用。这三个梯度缺一个,练习效果都会打折扣。

3.1 第一梯度:基本定义与调用——建立肌肉记忆

第一梯度是“看着题目就知道在考def语法”。这类题通常是:写一个函数计算两个数的和、写一个函数判断是否是质数、写一个函数统计字符串中某个字符出现的次数。难度低,但一定要亲手动笔写,而且要刻意练习几个动作:函数命名、参数命名、注释撰写。

命名这事儿看着不痛不痒,实际上很重要。我见过很多人写def f(x, y),过两天自己都看不懂。练习阶段就养成“参数名即文档”的习惯:def calc_area(radius)一眼就能看出算面积,def calc_area(r)就要靠猜。别小看这些小题目,它们是形成肌肉记忆的原材料。

# 示例题目:判断一个数是否为质数 def is_prime(num): """质数判断:大于1且只能被1和自身整除""" if num <= 1: return False if num == 2: return True if num % 2 == 0: return False for i in range(3, int(num ** 0.5) + 1, 2): if num % i == 0: return False return True

这道题哪怕你见过一百遍了,我也建议你亲手敲一遍,重点是想清楚“为什么只循环到根号num就好”——因为如果一个合数存在因子,那必然有一个因子小于等于它的平方根,这个优化思想在很多练习题里都会反复出现。

3.2 第二梯度:逻辑封装与代码复用——用函数收敛重复片段

第二梯度开始上强度了——题目里不再直接说“请写一个函数”,而是给你的代码里有一大段重复痕迹,让你自己定义函数来收敛。比如“把打印菜单、校验输入、重新打印这套逻辑提取出来”。这种题练的是“复用”意识,比单纯写一个递归找规律更有实用价值。

举个例子,一个简单但典型的复用题:统计一段文本中每个字符出现的频次,并且重复统计多段文本。

# 示例题目:字符频次统计函数,抽取为独立模块 def char_frequency(text): freq = {} for ch in text: freq[ch] = freq.get(ch, 0) + 1 return freq texts = ["hello world", "python practice", "function exercise"] for t in texts: result = char_frequency(t) top_char = max(result, key=result.get) print(f"{t!r} 里最先出现最多的字符是: {top_char}")

你看,如果没有函数,这段逻辑在三段文本上会重复三次,然后代码就变成一坨。抽成函数之后,不光可读性好,将来要改成“统计词频”“统计数字频次”,只要换掉内部逻辑就行,这就是复用的核心价值。这种练习题的训练重点,是让你在写代码前先问自己:“哪段逻辑要重复?该不该单独拎出来?”

3.3 第三梯度:递归与高阶应用——挑战调用自身的思维模式

递归这个话题,在函数练习题里永远是“看起来简单、写起来崩”。递归的精髓不在于“函数调用自己”这个行为,而在于“把大问题拆成小问题,直到小问题可以直接解决”这个思路。新手写递归最容易犯两个错:一是忘了写基线条件,导致无限递归;二是递归深度超出 Python 默认限制(大概是1000层),导致RecursionError。

这里想提醒的是:递归本身是一种“函数的另类用法”,在部分场景里(比如树的遍历、分治算法)无可替代。但日常业务开发里,能不用递归就不用递归,因为它的栈开销和可读性都不如循环。练习题里写递归,主要是为了打开思维。

# 示例题目:递归计算斐波那契数列的第n项 def fib(n): # 基线条件:第0项和第1项 if n <= 1: return n # 递归调用:自己调用自己 return fib(n - 1) + fib(n - 2) print(fib(10)) # 55

这个函数看着简单,但fib(30)就要等好几秒,fib(50)直接卡死,原因就是没有记忆化、大量重复计算。练习递归时一定要亲自感受一下这种“指数级变慢”的滋味,才能理解记忆化(比如用@functools.lru_cache)为什么存在。有条件的话,再对比一下循环版本的斐波那契,那才是实际项目里该用的写法。

我觉得,对这种综合类题目的理解深度,基本能把学习者分成三类:一类是把递归当“炫技”,一类是把递归当“基础”,还有一类是“虽然写得出但始终认为它只是工具”。练习题做多了,你会慢慢走向第三类。

4. 精选函数练习题解:代码示例、思路推导与运行结果

前面说了那么多,现在进入干货环节——我把自己在教学和带新人过程中,觉得最有代表性的函数练习题挑几道出来,带着你一步步分析、实现、验证。这一小节里的每道题,我都会先说思路,再给代码,最后标注易错点。

4.1 练习一:默认参数可变陷阱

这道题简直是函数练习题里的“照妖镜”。题目很简单:写一个函数,利用默认参数给列表加入新元素,返回新列表。

def append_element(element, target=[]): target.append(element) return target print(append_element(1)) # 输出 [1] print(append_element(2)) # 期望 [2]?实际 [1, 2] print(append_element(3)) # 期望 [3]?实际 [1, 2, 3]

这个结果让很多新手目瞪口呆:为什么第二次调用不重新创建一个空列表?因为target=[]这个默认列表在“函数定义时”只创建一次,之后每次调用都用同一个列表对象。这就是可变默认参数的经典陷阱。

应对方案只有一个:默认参数用None作哨兵,函数体内部再创建可变对象。

def append_element(element, target=None): if target is None: target = [] target.append(element) return target print(append_element(1)) # [1] print(append_element(2)) # [2] print(append_element(3)) # [3]

这道题的价值在于让你明白:函数定义时的默认值求值是一次性的,不是每次调用时重新求值。记住这个,你以后再也不会犯“默认参数存着上次运行结果”的错了。

4.2 练习二:局部函数与闭包概念

闭包在函数练习题里属于进阶内容,但值得提前感受。题目要求:写一个计数器函数,每次调用都让计数加一,并且多个计数器相互独立。

def make_counter(): count = 0 def counter(): nonlocal count count += 1 return count return counter c1 = make_counter() c2 = make_counter() print(c1()) # 1 print(c1()) # 2 print(c2()) # 1(独立计数)

这里的关键是nonlocal声明。如果去掉nonlocal count,counter函数里的count += 1会创建一个新的局部变量,导致“每次调用都报错说局部变量未定义”,或者总是返回1。nonlocal告诉 Python:“我修改的不是本作用域的变量,而是外层函数作用域里的count。”

这种“外层函数返回内层函数,内层函数持有外层变量”的结构,就是闭包。闭包在装饰器、延迟计算、函数式编程里大量出现。练习题里提前接触,能帮你在将来遇到装饰器时,不把它当成天书。

4.3 练习三:回调函数与排序规则定制

这个题目非常接近实际项目——把“函数作为参数传递”这个特性用起来。假设要按不同规则对一组字符串排序:按长度、按字母序、按字符串里数字的大小。

def sort_by_key(items, key_func): return sorted(items, key=key_func) words = ["python3", "go", "java8", "c11", "rust"] print(sort_by_key(words, len)) # 按长度: ['go', 'c11', 'rust', 'java8', 'python3'] print(sort_by_key(words, str.lower)) # 按字母序: ['c11', 'go', 'java8', 'python3', 'rust'] def number_part(s): digits = ''.join(filter(str.isdigit, s)) return int(digits) if digits else -1 print(sort_by_key(words, number_part)) # 提取数字作为排序键

你发现没有,sort_by_key函数根本不关心你传什么key_func,它只负责把key_func应用到每个元素上、按返回值排序。这种“把一个函数传给另一个函数”的模式,叫回调或者高阶函数。练习题到这里,已经从“语法学习”变成“编程思维”了。

我在带人时,通常会让他们先写一个最简单的冒泡排序,然后把比较规则抽成参数传进去。这个步骤做完,对“函数是一等公民”这个 Python 特性会有完全不同的理解。

4.4 练习四:递归实战——嵌套字典深度遍历

这道题是第三梯度里的压轴题型。题目是统计一个嵌套字典里所有“叶子节点”的总数,所谓叶子节点就是值不是字典的键值对。

def count_leaves(data): if not isinstance(data, dict): return 1 total = 0 for value in data.values(): total += count_leaves(value) return total nested = { "a": {"x": 1, "y": {"m": 2, "n": 3}}, "b": {"p": {"q": 4}}, "c": 5 } print(count_leaves(nested)) # 1,2,3,4,5 共 5 个叶子

这道题的关键是“递归出口”:如果当前数据不是字典,就返回1,说明找到一个叶子;如果是字典,就遍历所有值,把它们的叶子数加起来。你可能会问:“那如果是列表呢?”这就是考验你的扩展能力——再加一个isinstance(data, list)的分支即可。

递归练习题最忌讳的就是“背着模板写”。我当初学递归的最大体会是:必须自己在纸上画一遍调用树,把count_leaves(nested)如何一层层展开看清楚,才能理解递归的工作方式。也许你第一眼觉得抽象,但只要画过一遍树状展开,这个递归函数就永远刻在脑子里了。

5. 新手养老金级的避坑指南:函数练习题里的高频错误与排查思路

练习题做错不要紧,但错了以后找不到原因、只能靠百度“这个报错什么意思”,就很浪费时间。我根据经验,把新手在函数练习阶段最容易踩的坑列一份速查表。你做题时一旦遇到输出不对或者报错,先对照这张表排查。

5.1 高频错误速查表

典型错误现象常见原因排查方向
调用函数输出None函数体内只有print,没有return检查函数是否有返冑值;或return后没跟值
函数修改不了外部变量忘记global或nonlocal声明在函数内直接赋值,默认是创建新局部变量
默认参数喝上“上次的汤”默认参数用了可变对象(列表、字典)默认值改成None,函数体内部创建可变对象
传参顺序报错位置参数、默认参数、*args混用时顺序不对必须按“位置参数、默认参数、*args、**kwargs”顺序定义
递归卡死或报RecursionError缺少基线条件,或递归深度过大检查递归出口是否成立;考虑用循环替代
函数里调用了未定义的变量本作用域没有定义,又没加全局/外层声明确认变量被赋值的位置、赋值语句是否被执行

这张表是我在带新人时反复用到索引,每一条背后都有一堆翻车的现场。练题过程中对照着看,能在纠结中省下大量时间。

5.2 活用调试工具排查函数逻辑

很多人调试函数只靠print大法,其实在练习题阶段也够用,但一旦函数嵌套变深、逻辑变复杂,print就会满屏飞。我个人建议早点习惯标准库里的pdb调试器,或者至少在代码里用“有条件地打印”的方式。

比如对递归函数,可以加一个深度参数来打印调用层级:

def count_leaves(data, depth=0): indent = " " * depth print(f"{indent}进入: {data if not isinstance(data, dict) else 'dict...'}") if not isinstance(data, dict): return 1 total = sum(count_leaves(v, depth + 1) for v in data.values()) print(f"{indent}返回: {total}") return total

这样一跑立刻就能看到递归是按什么顺序展开的,以及每一层返回了什么结果。我不建议一上来就学特别高级的调试工具,但至少要会用“分层打印”——在函数入口处打印参数,在return处打印返回值,这比在函数体里乱打十行print有用得多。

5.3 从报错信息反推函数问题的思路

Python 的报错信息其实很直白。比如TypeError: flex_sum() missing 1 required positional argument: 'first',翻译过来就是调用时少给了必填参数;NameError: name 'count' is not defined,说明你引用了不存在的变量。

关键是不要只读报错的最后一行,要往上翻看“出错所在的行号”,以及在哪个函数内部。我见过太多人拿着NameError报错去改函数内部,结果问题其实出在调用方的变量名拼写错误。排查步骤应该是:先看哪一行报错,再看该行引用了什么名字,最后确认这个名字在当前作用域里是否定义过、赋值过。

6. 练习之后:把函数能力迁移到实际问题中的心得

函数练习题做完十几道,如果不写点小工具、不解决一个真实问题,很快就忘了。所以我的建议是:练完题立刻找一个“真实但小”的需求来做。比如把“统计一堆文本文件里的单词频次”写成一个模块化函数,或者把“从一堆杂乱的日志中提取关键信息”写成函数集合。

在这个迁移阶段,有几个心得是常规文档里不会写的:

第一,函数的第一版总是最丑陋的。新手不要想着一次写出完美函数,先让它跑通,再考虑优化。哪怕参数很多、逻辑堆在一起也别怕,跑通之后再拆子函数、再调整命名。先能跑,再变好,这是铁律。

第二,把打印输出和业务逻辑分离。很多人在写函数的时候,脑子里想的是“最后要打印什么”,于是函数里塞满print。可是当你要在另一个脚本里调用这个函数时,就发现没法拿到计算值。正确的做法是:函数只负责计算和返回,打印的事交给调用方。

第三,善用functools、itertools等标准库能大大简化函数体。比如统计词频用collections.Counter,扁平化嵌套列表可以用itertools.chain。练习题是为了让你理解底层机制,实战则应该追求高效优雅。两者并不矛盾——理解了底层,用高级工具才用得放心。

我自己带了这么多届新人,见过最明显的分水岭就是:能不能把一个复杂问题拆成几个小函数,然后把这些函数组合起来解决问题。那些只靠“背语法”的人是做不到这点的,但认真刷完一套设计良好的练习题的人,普遍能跨过这个坎。函数不只是 Python 的一个语法特性,它本质上是你组织自己思路的工具。练习题的价值,其实就在这里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询