1. 作业设计背后的教学逻辑:为什么函数总是选择题的重灾区
坦白说,绝大多数 Python 入门课程的第二次作业,都会安排函数专题,而且大概率会配上选择题。这看起来像是老师偷懒,实际上背后有明确的教学设计考量。
第一,函数是 Python 从“按顺序写代码”迈向“模块化组织代码”的分水岭。第一次作业通常还在讲变量、数据类型、运算符和简单的顺序结构,到了第二次作业,学习者必须开始适应“定义”和“调用”这种抽象思维。选择题恰恰是检测这种思维转变是否到位的高性价比方式,它能在有限的题目里覆盖大量边界情况,比让学生写一两个大程序更能暴露理解上的盲区。
第二,函数题目天然适合出选择题。因为函数涉及的概念非常细碎:形参和实参的对应关系、默认参数的计算时机、可变对象的引用传递、作用域的查找顺序、返回值的类型和数量……这些细节每个都是一个小坑,非常适合做成判断题式的选择题。
第三,从我的实际经验来看,函数选择题里那些看似简单的题目,往往能准确筛出两类人:一类是真理解了,一类是背住了语法但没消化。所以这篇博文不只是帮你把作业做对,更想帮你把函数这一章真正想明白。
2. 函数选择题的高频考点拆解:先搞清楚出题人想考什么
函数选择题不是随机乱出的,知识点高度集中。我把常见的考点归纳成六个大类,这也是你复习时应该按顺序逐个击破的主线。
2.1 定义与调用的基本匹配
这类题最简单也最基础,核心考的是:函数定义了几个参数,调用时传了几个实参,能不能对得上。别小看这种题,出错率一点都不低。
def greet(name, message): print(f"{message}, {name}") greet("张三", "早上好") # 正确,两个实参对应两个形参 greet("张三") # TypeError,少传了一个参数 greet("张三", "早上好", "额外") # TypeError,多传了一个参数关键点在于:Python 在调用函数时,实参数量必须和形参数量匹配(除非有默认参数或可变参数)。这里的“匹配”是在调用发生的瞬间检查的,不是在函数体执行时才检查。
2.2 形参与实参的传递方式
这是函数章节里最重要的核心概念之一。我的建议是把它上升到“变量绑定”的层面去理解,而不是停留在“传值还是传引用”的争论上。
def modify_list(lst): lst.append(4) my_list = [1, 2, 3] modify_list(my_list) print(my_list) # 输出 [1, 2, 3, 4]如果题目改成了lst = [4, 5, 6]这样重新赋值的写法,变量指向的对象就被替换了,对原列表没有影响。区分“修改对象内容”和“重新绑定变量名”,是打通这一类选择题的关键。
2.3 返回值的使用与常见误解
考的是return和print的区别,以及函数没有return时默认返回None。很多初学者会被“函数内部明明打印了结果”这件事误导,以为函数“返回”了那个值。
def add(a, b): print(a + b) result = add(1, 2) # 屏幕上会显示 3 print(result) # 输出 None记住:print是输出到终端,return是把值交还给调用方。选择题里只要出现“请问该函数的返回值是”,永远盯住return语句,忽略print的干扰。
2.4 作用域与局部变量的生命周期
考的是函数内部定义的变量在外部能不能访问、global关键字的作用、变量查找的 LEGB 顺序。这类题文字绕来绕去,考察本质就是一句话:函数内部的变量和函数外部的变量是两个世界。
x = 10 def change(): x = 20 change() print(x) # 输出 10,函数内部只是创建了局部变量 x什么时候能改外部变量?要么用global声明,要么别给同名变量重新赋值。这个知识点理解了,很多诸如“能否在函数内访问全局变量”的题都能秒杀。
2.5 默认参数与可变对象的陷阱
这是函数选择题里最经典的坑。默认参数只在函数定义时求值一次,之后每次调用都复用同一个对象。如果默认参数是列表、字典这种可变对象,就会出现问题。
def add_item(item, container=[]): container.append(item) return container print(add_item("a")) # ['a'] print(add_item("b")) # ['a', 'b'],同一个列表被复用了判断此类选择题时,我的技巧是:看到默认参数是[]或{},直接打上“坑题”的标记。正确写法是用None做默认值,函数内部再新建容器。
2.6 递归、Lambda 与嵌套函数
这三个属于进阶考点。递归考的是基线条件和递归条件的写法,lambda 考的是匿名函数的使用场景,嵌套函数考的是闭包和作用域链。这类题目通常放在选择题的末尾,用来区分高分档。
3. 用“代码笔记法”拆题:把选择题变成一次瘦身版的调试任务
很多同学做函数选择题的习惯是“读代码——在脑海里跑一遍——选答案”。但人脑模拟程序的可靠性很低,尤其遇到循环嵌套、递归这些语句,一步没想清楚,整条链路就断掉了。
我强烈推荐一个做法:随手画“执行轨迹笔记”。
具体操作分三步:
- 第一步,把函数调用时的参数代入过程写出来,清楚地标出哪个实参传给了哪个形参;
- 第二步,逐行执行函数体,遇到变量变化就更新笔记,绝不跳行、不凭“感觉”判断;
- 第三步,注意函数返回后,笔记里对应的调用文本应该怎样被替换成返回值,这样才好在外部继续推演。
这个方法本质上是把代码变成了一笔笔可以复查的账。一开始你觉得慢,但练熟了之后,准确率会明显提高。那种“好像选对了但说不清为什么”的状态会大幅减少。
举个例子:
def calculate(a, b=2, c=None): if c is None: c = a * b else: c = a + b return c print(calculate(3, c=1))按笔记法推演:a=3,b使用默认值2,c=1。由于c不是None,走else,c = 3 + 2 = 5,最终返回5。如果不把c=1这个键值对记下来,很容易误以为走的是第一分支,得到6。
选择题的四个选项往往就是针对这种粗心点设计的。做笔记的意义,就是把可能出错的地方逐一固定住。
4. 典型真题导读:穿行过几个种类的选择题实例
作业里具体出现什么题,每个版本不尽相同。但按上面的知识点分类,我们实际梳理几道代表性题目,方便大家了解每类问题的行进路径和判断流程。
4.1 参数匹配类例题
def demo(arg1, arg2="默认", *args): print(arg1, arg2, args) demo(1, 2, 3, 4)解题思路:arg1接收1,arg2接收2,多余的位置参数3, 4打包进args元组。所以输出是1 2 (3, 4)。
变化点在于:如果调用是demo(1, arg2="a", 3)呢?直接报SyntaxError。因为 Python 规定,位置参数不能出现在关键字参数之后,这种题目考的是参数语法的硬性规则。
4.2 作用域类例题
num = 5 def outer(): num = 10 def inner(): num = 20 print(num) inner() outer() # 输出 20如果你把inner里的num = 20去掉,那print(num)会先在inner局部作用域找,找不到再找outer的局部作用域(10),最后输出 10。这就是 LEGB:局部、嵌套、全局、内建。
需要注意,LEGB 中的“局部”是在代码中被赋值的那个函数体,而不是调用者的作用域。很多题的干扰项就是利用了“调用方环境”和“定义方环境”的不同。
4.3 递归类例题
def f(n): if n == 1: return 1 return n * f(n - 1) print(f(5))常规递推,最终算出 $5 \times 4 \times 3 \times 2 \times 1 = 120$。递归题最关键的是画“递归展开树”,一层层展开到底,再一层层回归。千万别只盯着return n * f(n - 1)这一行,忘了基线条件是n == 1时直接返回1。
4.4 可变默认参数类例题
def append_item(item, lst=[]): lst.append(item) return lst print(append_item(1)) print(append_item(2)) print(append_item(3, [0]))结果是:[1]、[1, 2]、[3, 0]还是什么样?详细展开一下。
第一次调用,默认列表变成[1];第二次调用,没有提供lst,使用的是同一个默认列表,变成[1, 2];第三次调用,显式传入了[0],不再使用默认列表,结果是[0, 3]。这个例子完整呈现了默认参数复用的特性,同时区分了默认列表和传入列表。
4.5 综合应用类例题
def func(x, y=[]): y.append(x) return y print(func(1)) # [1] print(func(2, [])) # [2],传了新的空列表,跟默认列表无关 print(func(3)) # [1, 3],默认列表仍保留着之前的 [1]这种综合题能把上面提到的知识点一次考全。做错了也不要紧,重点是把笔记法用起来,一行行展开,找到自己是在“第几次调用”时把状态跟丢的。
5. 我在批作业时常见的高频错误与排查策略
以下这些错误不是教材上的抽象说法,而是我在实际批改大量函数作业的过程中反复看到的真实情况。如果你做题时经常卡壳或者选错,多半是踩了其中某个坑。
5.1 在函数内部试图修改外部变量,却忘了用 global
count = 0 def increment(): count += 1 increment()这个代码直接抛UnboundLocalError,因为函数内部的count += 1相当于对局部变量count做了赋值,Python 会认为count是局部变量,而右侧count还没定义。很多选择题会把这个包装成一个“请问输出是几”的题,选项里甚至会有0、1这种数字,其实真实结果是直接报错。记住,在函数内部对一个变量赋值,它就会变成这个函数的局部变量——除非你用global声明。
5.2 混淆 return 与 print
出题人特别爱把一个“函数内部有 print 但没有 return”的题放在选择题里。正确的答案是返回值是None。如果你发现在哪道题上选了某个打印出来的数字而不是None,说明你在心理上默认了 print 的值可以被接收。这是一个需要立刻纠正的习惯。
5.3 默认参数的求值时机理解不透
有些同学以为调用add_item(1)时,默认参数lst=[]会“重新生成一个空列表”。这个认知是错的。默认参数在def语句执行时只创建一次,之后所有调用共享这个对象。这是 Python 语言层面最反直觉的行为之一,也是选择题的必考点。
5.4 形参和实参表达式搞混
def check(x): return x * 2 result = check(2 + 3)这里实参是表达式2 + 3,它会先被求值为5,再传给x。有些选项会写成2 + 3 * 2这种结果,明显是把求值顺序搞反了。实参传给函数的永远是“值”,不是“表达式本身”。
5.5 忽略函数内部再次调用函数
容易忘的是嵌套调用时返回值会一层层向外传递,中间任何一个函数没有return,最终结果就可能变成None。这种情况尤其容易出现在“装饰器题”里,但基础期的作业里,只要出现嵌套函数,就值得多画一层笔记。
6. 掌握函数选择题之后,怎么让这些能力在实战中起效
做选择题不是终点。函数这一章的选择题,本质上是为你后续写“真正的项目代码”做铺垫的。我总结了一下,这个章节的知识与后续实战的对应关系大概是这样的:
- 默认参数与可变对象:写函数时,如果默认值是容器类型,一定用
None加内部判断。 - 作用域规则:写大一点的程序时,尽量避免到处用
global,而是通过参数传递和返回值来管理数据。 - 参数传递方式:写函数时明确你是想修改传入对象本身,还是想返回一个新对象,这个意图清楚了,接口设计也就清楚了。
- 递归思想:后续学习数据结构和算法时会大量用到,函数学习阶段把递归基础打好,后面学树的遍历、二分查找时会轻松得多。
- lambda 与嵌套函数:后续学高阶函数(如
map、filter、sorted的key参数)时会频繁使用。
我个人建议,在做完这份选择题作业之后,再额外写三个小练习巩固一下:一个用函数求任意数字列表的平均值;一个写一个函数判断字符串是否是回文;一个用递归写一个斐波那契数列函数。这三个小练习覆盖了参数、返回值、循环、递归,做完了,函数这一章的基本功就算真正落地了。
函数本质上就是把复杂问题拆成小块,每块有一个明确的输入和输出。选择题只是检验你是否理解了这些“块”的边界和连接方式。把边界和连接方式理顺了,后面的路会顺很多。