五、函数与作用域(P41–P52)
P41 · 知识点:函数定义
【题目】Python 中定义函数使用的关键字是?
A. function B. def C. func D. lambda
答案:B
【考点】def 定义具名函数,lambda 定义匿名函数。
【结论】选 B。Python 使用 def 关键字声明具名函数。
【逐项辨析】 A. function:错误。function 是 JavaScript 等语言的关键字,并非 Python 的关键字。 B. def:正确。def 是 Python 中定义具名函数的标准关键字。 C. func:错误。func 不是 Python 关键字,常见于其他语言或作为变量名使用。 D. lambda:错误。lambda 用于定义匿名单行函数,而非具名函数。
【知识点】在 Python 中,函数是第一类对象。def 语句会创建一个函数对象并将其绑定到指定名称。函数定义语法为 def 函数名(参数列表): 函数体。函数体必须缩进。lambda 表达式则是创建匿名函数对象的另一种方式,语法为 lambda 参数: 表达式,只能包含单个表达式,不能包含语句或注解。Python 的关键字列表可通过 keyword.kwlist 查看,其中不包含 function 或 func。
【记忆锚点】“定义函数用 def,匿名简写 lambda;function 是 JS 的,func 只是昵称别犯傻。”
【易混对比】
| 概念 | 关键字 | 适用场景 | 是否有函数名 |
|---|---|---|---|
| 具名函数 | def | 多行逻辑、复用性高 | 有 |
| 匿名函数 | lambda | 单行简单表达式、临时使用 | 无 |
| 其他语言函数 | function | JavaScript、C 等 | 视语言而定 |
换问法: 若题目改为“Python 中定义匿名函数使用的关键字是?”,则应选择 D(lambda)。
【自测】 以下代码的输出是什么?
def add(a, b):
return a + b
print(add(2, 3))答:5。因为 def 定义了具名函数 add,调用时传入 2 和 3,返回 5。 与第 P44 题连考。
【知识关联】
- 同库关联:与 P49(lambda)、P42–P48 函数题群;与 Java 方法定义对照。
- 实现层:def 创建函数对象(PyFunctionObject)绑定到名字;code 对象含字节码。
- 面试追问:① 函数是第一类对象意味着什么?② 递归默认深度?
【拓展延伸】
- 变式问法:
type(f);能否把函数赋值给变量。 - 版本差异:语义稳定;3.x 注解更完善。
- 工程注意点:短函数命名清晰;避免过深递归(sys.setrecursionlimit 慎改)。
P42 · 知识点:参数传递
【题目】Python 函数参数传递的方式是?
A. 值传递 B. 引用传递 C. 对象引用传递(传对象引用:可变对象可被函数内修改,不可变对象不行) D. 指针传递
答案:C
【考点】Python 传参语义:按对象引用(传对象,不是传值的拷贝也不是传变量的引用)。
【结论】选 C。Python 的参数传递机制是“按对象引用传递”(传对象引用)。
【逐项辨析】 A. 值传递:错误。纯值传递会复制整个实参的值,而 Python 并未拷贝对象本身,只是让形参变量指向该对象。 B. 引用传递:错误。纯引用传递意味着形参就是实参的别名,对形参重新赋值也会影响外部变量;但 Python 中对形参重新绑定不会改变外部变量的指向。 C. 对象引用传递:正确。Python 将实参对象的引用赋值给形参,形参与实参指向同一对象;可变对象原地修改会对外可见,不可变对象重新绑定则不会。 D. 指针传递:错误。Python 中没有显式指针概念,虽然底层实现涉及指针,但语义层面不使用此术语。
【知识点】Python 的参数传递语义称为“call by object reference”或“call by sharing”。具体表现为:调用函数时,实参对象的引用被复制给形参,因此形参和实参引用同一个对象。对于不可变对象(如 int、str、tuple、frozenset),任何“修改”操作实际上都是创建新对象并让形参重新绑定,因此外部实参不受影响。对于可变对象(如 list、dict、set),如果通过方法进行原地修改(如 list.append、dict.update),由于对象本身被修改且外部引用仍指向该对象,修改对外可见;但如果对形参进行赋值操作(如 a = a + [1]),则只是让形参指向新对象,不会影响外部。这一区别是理解 Python 内存模型和函数副作用的关键。
【记忆锚点】“可变对象原地改,内外一起变;不可变对象重新绑,外边看不见。Python 传参传的是'对象的地址复印件'。”
【易混对比】
| 传参方式 | 是否拷贝对象 | 重新绑定是否影响外部 | 原地修改是否影响外部 | Python 对应场景 |
|---|---|---|---|---|
| 值传递 | 是(深拷贝) | 否 | 否 | 无 |
| 引用传递 | 否 | 是 | 是 | 无 |
| 对象引用传递 | 否(拷贝引用) | 否 | 可变对象:是;不可变对象:N/A | Python 实际机制 |
换问法: 若题目改为“执行 def f(a): a.append(1) 后外部列表是否会改变?”,答案是会改变,因为 list 是可变对象且 append 属于原地修改。
【自测】
def demo(x, y):
x = x + 1
y.append(1)
a, b = 0, []
demo(a, b)
print(a, b)答:0 [1]。因为 a 是不可变对象 int,函数内 x = x + 1 让形参 x 指向新对象 1,不影响外部 a;b 是可变对象 list,y.append(1) 原地修改了列表对象本身,因此外部 b 变为 [1]。 与第 P43 题连考。
【知识关联】
- 同库关联:与 P22(引用)、P43(默认参数)、P67–P69(拷贝);与 Java 引用传递对照。
- 实现层:call by object reference;形参是新名字绑定同一对象。
- 面试追问:① 如何在函数内改外部可变对象?② 不可变对象能否「改」?
【拓展延伸】
- 变式问法:列表 append 内外可见;整型重绑不可见。
- 版本差异:语义稳定。
- 工程注意点:显式返回修改结果更清晰;注意默认参数可变(P43)。
P43 · 知识点:可变默认参数
【题目】连续调用的输出结果是?
def f(x, L=[]):
L.append(x)
return L
print(f(1))
print(f(2))
print(f(3))A. [1]、[1, 2]、[1, 2, 3] B. [1]、[2]、[3] C. 抛异常 D. [1]、[1]、[1]
答案:A
【考点】可变默认参数的经典陷阱:默认值只在函数定义时求值一次。
【结论】选 A。函数定义时默认参数只被求值一次,导致多次调用共享同一个列表对象。
【推导过程】 函数定义 def f(x, L=[]) 在定义阶段执行时,会创建一次列表对象 [],并将其绑定为参数 L 的默认值。后续每次调用 f(1)、f(2)、f(3) 且未提供 L 时,形参 L 都引用这同一个列表对象。
- 第一次调用
f(1):L.append(1)→ 该列表变为[1],返回[1]。 - 第二次调用
f(2):L 仍指向同一列表,L.append(2)→[1, 2],返回[1, 2]。 - 第三次调用
f(3):L.append(3)→[1, 2, 3],返回[1, 2, 3]。
因此三次输出依次为 [1]、[1, 2]、[1, 2, 3]。
【逐项辨析】 A. [1]、[1, 2]、[1, 2, 3]:正确。默认列表对象被共享,每次 append 都在同一对象上累积。 B. [1]、[2]、[3]:错误。该结果只有在每次调用都创建新列表时才会出现,但默认参数不会在每次调用时重新求值。 C. 抛异常:错误。代码语法完全合法,不会引发异常。 D. [1]、[1]、[1]:错误。这要求每次 append 后列表被重置,但代码中不存在重置逻辑。
【知识点】Python 的默认参数在函数定义时(而非调用时)求值,且仅求值一次。对于可变默认值(如列表、字典、集合),这会导致所有缺省调用共享同一对象,从而引发意外的状态累积。这是 Python 最著名的“坑”之一。安全做法是使用不可变对象 None 作为占位符,在函数体内部按需创建新可变对象:
def f(x, L=None):
if L is None:
L = []
L.append(x)
return L此模式确保了每次缺省调用都获得独立的新列表。
【记忆锚点】“默认参数求值早,定义一次坑到老;可变对象别当默认值,None 占位函数里造。”
【易混对比】
| 写法 | 默认值类型 | 是否每次创建新对象 | 是否安全 |
|---|---|---|---|
def f(L=[]) | 可变(list) | 否 | 否(共享状态) |
def f(L=None) + 内部判断 | 不可变(None) | 是(函数内新建) | 是 |
def f(L=()) | 不可变(tuple) | 定义时一次 | 是(但无法原地修改) |
换问法: 若题目改为“以下哪种写法能避免默认参数共享问题?”,正确答案应为 def f(x, L=None): if L is None: L = []。
【自测】
def add_end(L=[]):
L.append('END')
return L
print(add_end([1, 2]))
print(add_end())
print(add_end())答:[1, 2, 'END'] 后两次分别为 ['END'] 和 ['END', 'END']。因为第一次传入了自定义列表,不影响默认值;后两次使用默认列表,共享并累积 'END'。 与第 P42 题连考。
【知识关联】
- 同库关联:与 P42 强相关;面试必考「可变默认参数」。
- 实现层:默认值在 def 时求值一次,存在函数对象的 defaults 元组中。
- 面试追问:① 安全写法?② 默认值为 None 为何安全?
【拓展延伸】
- 变式问法:连续调用累积;
def f(x, L=[])输出。 - 版本差异:语义稳定。
- 工程注意点:可变默认一律 None 占位;代码审查重点项。
P44 · 知识点:可变参数
【题目】def f(*args, **kwargs): 中 *args 和 **kwargs 分别收集?
A. 多余的位置参数(元组)和多余的关键字参数(字典) B. 位置参数(列表)和关键字参数(元组) C. 都收集为元组 D. 都收集为字典
答案:A
【考点】*args 收集位置参数为元组,**kwargs 收集关键字参数为字典。
【结论】选 A。*args 收集多余位置参数为元组,**kwargs 收集多余关键字参数为字典。
【逐项辨析】 A. 多余的位置参数(元组)和多余的关键字参数(字典):正确。*args 产生 tuple,**kwargs 产生 dict。 B. 位置参数(列表)和关键字参数(元组):错误。收集结果不是列表和元组,且它们只收集“多余”的参数,而非所有参数。 C. 都收集为元组:错误。只有 *args 收集为元组,**kwargs 收集为字典。 D. 都收集为字典:错误。只有 **kwargs 收集为字典。
【知识点】在 Python 函数定义中,*args 和 **kwargs 是用于接收不定数量参数的标准语法。*args 中的星号表示将多余的位置参数打包成一个元组(tuple),形参名通常使用 args,但可自定义;**kwargs 中的双星号表示将多余的关键字参数打包成一个字典(dict),键为参数名字符串,值为传入值。在 Python 3 中,*args 还可以用于强制要求后续参数必须以关键字形式传递,例如 def f(a, *, b) 中 b 只能关键字传入。此外,* 和 ** 不仅用于函数定义,也可用于调用时的解包操作:func(*iterable) 将可迭代对象解包为位置参数,func(**mapping) 将映射解包为关键字参数。
【记忆锚点】“一颗星打包位置变元组,两颗星打包关键字变字典;调用时解包,定义时收集。”
【易混对比】
| 语法 | 使用场景 | 收集结果 | 参数形式 |
|---|---|---|---|
*args | 函数定义 | 元组 tuple | 多余位置参数 |
**kwargs | 函数定义 | 字典 dict | 多余关键字参数 |
*iterable | 函数调用 | 解包为位置参数 | 调用时展开 |
**mapping | 函数调用 | 解包为关键字参数 | 调用时展开 |
换问法: 若题目改为“调用 f(*(1, 2), **{'a': 3}) 时星号的作用是什么?”,答案是在调用端进行解包,将元组和字典展开为独立参数。
【自测】 以下代码输出什么?
def func(a, b, *args, c=10, **kwargs):
print(args, kwargs)
func(1, 2, 3, 4, c=20, x=100)答:
(3, 4) {'x': 100}。因为 1、2 分别绑定给 a、b;3、4 被*args收集为元组;c=20 是关键字参数;x=100 被**kwargs收集为字典。 与第 P45 题连考。
【知识关联】
- 同库关联:与 P45(关键字参数);与 Java 可变参数对照。
- 实现层:
*args收集为元组;**kwargs收集为字典。 - 面试追问:① 参数顺序规则?② 如何「打散」传递?
【拓展延伸】
- 变式问法:
f(*L, **d);仅关键字参数*后。 - 版本差异:3.8 positional-only
/;3.0 keyword-only*。 - 工程注意点:包装器/装饰器常用;API 设计避免过多可变参数。
P45 · 知识点:参数类型
【题目】调用 f(a=1, b=2) 时,传参方式属于?
A. 位置参数 B. 关键字参数 C. 默认参数 D. 可变参数
答案:B
【考点】位置参数与关键字参数的概念。
【结论】选 B。以 参数名=值 形式传参属于关键字参数。
【逐项辨析】 A. 位置参数:错误。位置参数是按顺序直接写值(如 f(1, 2)),不带参数名。 B. 关键字参数:正确。调用时使用 a=1, b=2 的形式即为关键字参数,可打乱顺序。 C. 默认参数:错误。默认参数是在函数定义时给形参指定默认值(如 def f(a=1)),属于形参的属性,而非调用时的传参方式。 D. 可变参数:错误。可变参数指 *args 或 **kwargs,用于接收不定数量的参数。
【知识点】Python 函数的参数在调用时可分为位置参数和关键字参数两大类。位置参数按声明顺序匹配;关键字参数按参数名匹配,且允许与位置参数混用,但位置参数必须在前。在函数定义侧,参数可分为:必需参数(无默认值)、默认参数(有默认值)、可变参数(*args)、命名关键字参数(* 后的参数)和关键字可变参数(**kwargs)。调用时 f(a=1, b=2) 中的 a=1 是关键字参数调用方式,而定义时 def f(a=1) 中的 a=1 是默认参数定义方式,二者处于不同阶段,概念不可混淆。
【记忆锚点】“调用时写名等于值,这叫关键字;定义时写名等于值,这叫默认值。一个调用,一个定义,千万别搞混。”
【易混对比】
| 概念 | 所处阶段 | 形式 | 作用 |
|---|---|---|---|
| 位置参数 | 调用 | f(1, 2) | 按顺序匹配形参 |
| 关键字参数 | 调用 | f(a=1, b=2) | 按名称匹配,顺序无关 |
| 默认参数 | 定义 | def f(a=1) | 调用时可省略该参数 |
| 可变参数 | 定义 | def f(*args, **kwargs) | 接收不定数量参数 |
换问法: 若题目改为“函数定义中 def f(a=1) 的 a=1 属于什么参数类型?”,则应选择默认参数(C)。
【自测】 以下哪种调用方式是合法的?
def f(a, b, c=3):
pass选项:A. f(1, c=3, b=2) B. f(a=1, 2, c=3) C. f(1, b=2, 3) D. f(c=3, 1, 2)
答:A。Python 规定位置参数必须在关键字参数之前,只有 A 满足此规则。 与第 P44 题连考。
【知识关联】
- 同库关联:与 P44;positional/keyword/default 分类。
- 实现层:绑定规则与错误类型(TypeError)。
- 面试追问:① 位置参数在关键字之后?② 默认参数陷阱?
【拓展延伸】
- 变式问法:
f(a=1, 2)非法;调用时关键字顺序可变。 - 版本差异:3.8
/标记仅位置。 - 工程注意点:公共 API 优先关键字参数提高可读性。
P46 · 知识点:作用域
【题目】输出结果是?
x = 10
def f():
x = 5
f()
print(x)A. 10 B. 5 C. 抛异常 D. None
答案:A
【考点】函数内的赋值创建局部变量,不影响全局变量。
【结论】选 A。函数内部的赋值操作创建局部变量,不影响同名的全局变量。
【推导过程】
- 全局作用域中执行
x = 10,创建全局变量x,绑定到整数对象 10。 - 定义函数
f,其内部执行x = 5。由于该语句对x进行了赋值操作,Python 编译时将x标记为f的局部变量。 - 调用
f()时,函数内部创建局部变量x并绑定到 5,此操作完全在局部命名空间中进行,与全局x无关。 - 函数返回后,全局变量
x仍然指向 10。 - 执行
print(x)时,按照 LEGB 规则在全局作用域中找到x,输出 10。
【逐项辨析】 A. 10:正确。函数内的赋值创建了局部变量,不影响全局变量。 B. 5:错误。只有在 print(x) 位于函数内部,或使用 global 声明时才会输出 5。 C. 抛异常:错误。代码逻辑完全合法,不会引发异常。 D. None:错误。f() 虽然没有显式返回值,但 print(x) 打印的是全局 x,而非函数返回值。
【知识点】Python 的作用域遵循 LEGB 查找规则:Local(局部)→ Enclosing(嵌套函数的外层)→ Global(全局)→ Built-in(内建)。当函数体内出现对某个变量的赋值语句时,Python 默认将其视为局部变量(除非显式声明 global 或 nonlocal)。这种现象称为“变量遮蔽”(shadowing)。如果需要在函数内修改全局变量,必须使用 global x 声明;否则赋值操作仅在局部命名空间中创建新绑定,不影响外层同名变量。理解这一机制对于避免在函数中意外创建局部变量而导致 UnboundLocalError 至关重要。
【记忆锚点】“函数里一赋值,局部就出生;要想改全局,global 来敲门。LEGB 像漏斗,从内往外搜。”
【易混对比】
| 场景 | 是否需要声明 | 作用域影响 | 典型错误 |
|---|---|---|---|
| 读取全局变量 | 不需要 | 无 | 无 |
| 修改全局变量(赋值) | 需要 global | 全局被修改 | 不加 global 则创建局部变量,外部不变 |
| 修改外层函数变量 | 需要 nonlocal | 外层被修改 | 不加 nonlocal 会报 UnboundLocalError |
换问法: 若题目改为“若想在函数内将全局变量 x 修改为 5,应如何写?”,答案是在函数内添加 global x 后再赋值。
【自测】
count = 0
def increment():
count += 1
increment()上述代码执行结果是什么?
答:抛出
UnboundLocalError。因为count += 1使 count 成为局部变量,但局部作用域中 count 未被初始化就先读取,导致报错。如需修改全局 count,应在函数内第一行声明global count。 与第 P47 题连考。
【知识关联】
- 同库关联:与 P47(global)、P48(nonlocal)、LEGB 规则。
- 实现层:Local→Enclosing→Global→Builtins 查找。
- 面试追问:① 未声明 global 赋值会怎样(本地新绑定)?② 闭包捕获?
【拓展延伸】
- 变式问法:内层函数读外层变量;赋值不加声明。
- 版本差异:语义稳定。
- 工程注意点:避免全局可变状态;模块级常量用全大写。
P47 · 知识点:global
【题目】输出结果是?
x = 1
def f():
global x
x += 1
f()
print(x)A. 2 B. 1 C. 抛异常 D. None
答案:A
【考点】global 声明使函数内绑定指向全局变量。
【结论】选 A。global 声明使函数内对 x 的操作指向全局变量,执行后全局 x 变为 2。
【推导过程】
- 全局作用域执行
x = 1,全局变量x绑定到整数 1。 - 函数
f内部第一行声明global x,明确告知 Python:f内对x的所有操作均作用于全局变量。 - 执行
x += 1,即x = x + 1。由于global声明,读取的x是全局变量 1,加 1 后赋值给全局x,全局x更新为 2。 - 调用
f()后,全局x的值已变为 2。 print(x)输出全局x,结果为 2。
【逐项辨析】 A. 2:正确。global 声明后,x += 1 修改的是全局变量,1 + 1 = 2。 B. 1:错误。若未声明 global,函数内对 x 的赋值会创建局部变量,但 x += 1 需要先读取,会导致 UnboundLocalError,不会静默保持 1。 C. 抛异常:错误。代码中已正确声明 global,不会抛出异常。 D. None:错误。print(x) 打印的是整数 2,而非 None。
【知识点】global 是 Python 的关键字,用于在函数内部声明某个变量为全局变量。一旦声明,函数内对该变量的读取和写入都直接操作全局命名空间中的绑定。与之相对,如果没有 global 声明且函数内对变量有赋值操作,Python 会在编译阶段将该变量识别为局部变量。此时若出现 x += 1 这类先读后写的操作,由于局部变量尚未绑定初值,运行时就会抛出 UnboundLocalError。global 一般应谨慎使用,过度使用会破坏函数的封装性和可测试性;对于需要跨函数共享状态的场景,更推荐通过类属性、闭包或显式传参来管理。
【记忆锚点】“函数想改全局量,global 必须放头上;不写 global 就赋值,UnboundLocal 来挡路。”
【易混对比】
| 声明 | 作用范围 | 适用场景 | 常见错误 |
|---|---|---|---|
global x | 当前函数的整个局部作用域 | 修改全局模块级变量 | 忘记声明导致 UnboundLocalError |
nonlocal x | 当前函数的外层(非全局)函数 | 修改嵌套闭包中的变量 | 用于全局变量会报 SyntaxError |
换问法: 若题目改为“去掉 global x 后执行结果是什么?”,答案是抛出 UnboundLocalError。文案有版本差异:Python 3.11 起为 cannot access local variable 'x' where it is not associated with a value(本机 3.12.3 实测),3.10 及更早才是 local variable 'x' referenced before assignment;异常类型不变,别因文案不同以为换了异常。
【自测】
total = 100
def discount(rate):
global total
total = total * (1 - rate)
discount(0.2)
print(total)答:80.0。global 声明后,total 指向全局变量,计算 100 * 0.8 = 80.0,全局 total 被更新。 与第 P48 题连考。
【知识关联】
- 同库关联:与 P46/P48;global 声明后赋值改全局。
- 实现层:编译为 STORE_GLOBAL 字节码。
- 面试追问:① global 能否只读不声明?② 与可变对象原地改?
【拓展延伸】
- 变式问法:函数内
global x; x=1。 - 版本差异:语义稳定。
- 工程注意点:少用 global;需要共享状态用类或模块单例对象。
P48 · 知识点:闭包与 nonlocal
【题目】调用 outer()() 会?
def outer():
n = 1
def inner():
n += 1
return n
return innerA. 返回 2 B. 抛出 UnboundLocalError(修改外层变量需 nonlocal 声明) C. 返回 1 D. 返回 None
答案:B
【考点】闭包内修改外层函数变量必须用 nonlocal。
【结论】选 B。内层函数中修改外层变量但未声明 nonlocal,会抛出 UnboundLocalError。
【推导过程】
- 调用
outer()时,创建局部变量n = 1。 - 定义内层函数
inner。由于inner引用了外层变量n,理论上形成了闭包。 inner内部执行n += 1。该语句等价于n = n + 1,包含对n的读取和赋值。- Python 编译
inner时,发现函数体内有对n的赋值操作,因此将n判定为inner的局部变量。 - 运行时执行
n + 1,需要读取n的值,但此时inner的局部命名空间中尚未绑定n,因此抛出UnboundLocalError。异常文案按版本区分:Python 3.11 起为cannot access local variable 'n' where it is not associated with a value(本机 3.12.3 实测),3.10 及更早才是local variable 'n' referenced before assignment——类型始终是 UnboundLocalError(属 NameError 的子类)。 - 若要在
inner中正确修改外层n,必须在inner第一行添加nonlocal n。
【逐项辨析】 A. 返回 2:错误。只有在声明 nonlocal n 后,n += 1 才能成功修改外层变量并返回 2。 B. 抛出 UnboundLocalError(修改外层变量需 nonlocal 声明):正确。内层赋值导致 n 被视为局部变量,但局部 n 未初始化先被读取,触发异常。 C. 返回 1:错误。代码不会正常执行到 return n,而是在 n += 1 处就已经抛出异常。 D. 返回 None:错误。函数因异常中断,不会返回 None。
【知识点】闭包(closure)是指嵌套函数引用了其外层作用域中的变量,并且该嵌套函数被作为返回值传递或延迟执行的现象。在 Python 中,读取外层变量可以自动形成闭包,但要修改外层(非全局)变量,必须使用 nonlocal 声明。nonlocal 告诉 Python:该变量不属于当前局部作用域,请向外层非全局作用域查找。如果外层没有该变量,则抛出 SyntaxError。nonlocal 与 global 的区别在于:global 始终指向模块级全局变量,而 nonlocal 指向最近的 enclosing 作用域中的变量。
【记忆锚点】“闭包读取自动成,修改必须 nonlocal 声;global 指向模块顶,nonlocal 指外层名。”
【易混对比】
| 场景 | 所需声明 | 指向目标 | 若目标不存在 |
|---|---|---|---|
| 修改全局变量 | global | 模块级全局命名空间 | 在全局创建新变量 |
| 修改外层函数变量 | nonlocal | 最近的 enclosing 作用域 | 抛出 SyntaxError |
| 只读外层变量 | 无 | 通过闭包引用 | 无 |
换问法: 若题目改为“在 inner 中如何正确修改 outer 的 n 并返回 2?”,答案是添加 nonlocal n 声明后再执行 n += 1。
【自测】
def make_counter():
count = 0
def counter():
nonlocal count
count += 1
return count
return counter
c = make_counter()
print(c(), c())答:1 2。nonlocal 使得 counter 函数能够修改外层 make_counter 中的 count,每次调用累加 1,第一次返回 1,第二次返回 2。 与第 P46 题连考。
【知识关联】
- 同库关联:与 P46/P47、P74(装饰器依赖闭包)。
- 实现层:闭包单元 cell 对象;nonlocal 写 enclosing 变量。
- 面试追问:① 闭包延迟绑定陷阱?② 与 Java 内部类捕获对比?
【拓展延伸】
- 变式问法:循环中 lambda 捕获循环变量(默认延迟)。
- 版本差异:语义稳定。
- 工程注意点:默认参数绑定当前值避免延迟坑;装饰器必备。
P49 · 知识点:lambda
【题目】(lambda x: x * 2)(5) 的结果是?
A. 10 B. 25 C. 抛异常 D. 5
答案:A
【考点】lambda 匿名函数与立即调用。
【结论】选 A。lambda 表达式定义匿名函数后立即调用,传入 5 返回 10。
【推导过程】 lambda 表达式 lambda x: x * 2 创建了一个接收参数 x 并返回 x * 2 的匿名函数对象。紧接着的 (5) 表示立即调用该函数,传入实参 5。计算 5 * 2 = 10,因此最终结果为 10。
【逐项辨析】 A. 10:正确。lambda x: x * 2 等价于函数 f(x) = x * 2,传入 5 得 5 * 2 = 10。 B. 25:错误。25 是 5 ** 2 的结果,而本题是乘法而非乘方。 C. 抛异常:错误。lambda 语法正确且调用方式合法,不会抛出异常。 D. 5:错误。5 是输入值,而非函数返回值。
【知识点】lambda 表达式是 Python 中创建匿名函数对象的简洁方式,语法为 lambda [参数列表]: 表达式。其求值结果是一个函数对象,可立即调用,也可赋值给变量或作为参数传递。lambda 的主体必须是单个表达式,不能包含语句(如赋值 =、return、yield、if/else 语句块等),但可以使用条件表达式(如 x if x > 0 else 0)。lambda 常用于高阶函数(如 map、filter、sorted 的 key 参数)的简短回调场景。需要注意的是,虽然可以用 lambda 做立即调用(IIFE),但在 Python 中这种方式并不常见,通常直接用表达式即可。
【记忆锚点】“lambda 匿名一行清,参数冒号表达式;没有语句没名字,map filter 里最常见。”
【易混对比】
| 特性 | lambda | def 定义的函数 |
|---|---|---|
| 是否有名称 | 无(匿名) | 有(绑定到函数名) |
| 函数体内容 | 单个表达式 | 任意多行语句 |
| 是否包含 return | 表达式结果自动返回 | 需要显式 return |
| 适用场景 | 简单临时逻辑 | 复杂、复用性高的逻辑 |
换问法: 若题目改为“lambda 函数体中可以包含多条语句吗?”,答案是不可以,lambda 只能包含单个表达式。
【自测】 将以下 lambda 转换为等价的 def 函数:
max_key = lambda d: max(d, key=d.get)答:
def max_key(d):
return max(d, key=d.get)lambda 的参数列表对应 def 的形参,冒号后的表达式对应 return 的内容。 与第 P50 题连考。
【知识关联】
- 同库关联:与 P41/P50/P51;匿名函数单表达式。
- 实现层:编译为函数对象;无注解/多语句。
- 面试追问:① lambda 能否写语句?② 与 def 性能差异?
【拓展延伸】
- 变式问法:
sorted(key=lambda x: x[1]);立即调用。 - 版本差异:语义稳定。
- 工程注意点:复杂逻辑用 def;排序 key 常用 lambda。
P50 · 知识点:map
【题目】list(map(lambda x: x ** 2, [1, 2, 3])) 的结果是?
A. [1, 4, 9] B. [1, 2, 3] C. (1, 4, 9) D. 抛异常
答案:A
【考点】map 对可迭代对象每个元素应用函数,返回迭代器。
【结论】选 A。map 将平方函数应用于列表每个元素,list 转换后得到 [1, 4, 9]。
【推导过程】map(lambda x: x ** 2, [1, 2, 3]) 将 lambda 函数依次应用于列表 [1, 2, 3] 的每个元素:
- 对 1 应用:1 ** 2 = 1
- 对 2 应用:2 ** 2 = 4
- 对 3 应用:3 ** 2 = 9 map 返回一个迭代器,产生序列 1, 4, 9。经
list()收集后得到[1, 4, 9]。
【逐项辨析】 A. [1, 4, 9]:正确。1²=1、2²=4、3²=9,map 返回的迭代器经 list() 收集为列表。 B. [1, 2, 3]:错误。这是原始列表,未经过平方映射。 C. (1, 4, 9):错误。map 返回的是迭代器,经 list() 转换后是列表而非元组;若要得到元组需使用 tuple()。 D. 抛异常:错误。lambda 和 map 的用法均合法,不会引发异常。
【知识点】map(function, iterable, ...) 是 Python 的内置高阶函数,属于函数式编程工具。它将指定函数依次应用于可迭代对象的每个元素,并返回一个迭代器(在 Python 3 中),具有惰性求值特性,只有在迭代时才会逐个计算结果。这种设计节省了内存,尤其适合处理大规模数据。若需要完整的结果集合,通常需要显式转换为 list、tuple 或 set。在 Python 2 中,map 直接返回列表。对于简单映射操作,列表推导式(如 [x**2 for x in [1,2,3]])往往比 map+lambda 更易读,但 map 在已有具名函数时(如 map(len, strings))仍然很优雅。
【记忆锚点】“map 映射逐个算,返回迭代器不直接给清单;想要列表外面套 list,惰性求值省空间。”
【易混对比】
| 函数 | 返回值类型(Python 3) | 作用 | 转换方式 |
|---|---|---|---|
| map | 迭代器 iterator | 对每个元素应用函数 | list(map(...)) |
| filter | 迭代器 iterator | 保留满足条件的元素 | list(filter(...)) |
| zip | 迭代器 iterator | 将多个可迭代对象按索引打包 | list(zip(...)) |
换问法: 若题目改为“map(lambda x: x**2, [1, 2, 3]) 直接返回什么类型?”,答案是 map 对象(一种迭代器),而非列表。
【自测】 以下两段代码结果是否相同?
a = list(map(str, [1, 2, 3]))
b = [str(x) for x in [1, 2, 3]]答:结果相同,均为
['1', '2', '3']。但 map 在已有具名函数 str 时效率略高且更简洁;列表推导式在需要复杂逻辑时更易读。 与第 P51 题连考。
【知识关联】
- 同库关联:与 P17(推导式)、P51;Py3 map 返回迭代器。
- 实现层:惰性;list(map(...)) 物化。
- 面试追问:① 与推导式选择?② 多序列 map?
【拓展延伸】
- 变式问法:
map(int, ["1","2"]);list(map(...))。 - 版本差异:Py3 惰性 vs Py2 列表。
- 工程注意点:可读性优先推导式;大流可用 map 省中间列表。
P51 · 知识点:filter
【题目】list(filter(lambda x: x % 2 == 0, range(5))) 的结果是?
A. [0, 2, 4] B. [1, 3] C. [0, 1, 2, 3, 4] D. [2, 4]
答案:A
【考点】filter 保留使函数返回 True 的元素。
【结论】选 A。filter 保留满足条件的偶数元素,range(5) 中的偶数为 0、2、4。
【推导过程】filter(lambda x: x % 2 == 0, range(5)) 将 lambda 函数依次作用于 range(5) 产生的 0, 1, 2, 3, 4:
- 0 % 2 == 0 → True,保留
- 1 % 2 == 0 → False,剔除
- 2 % 2 == 0 → True,保留
- 3 % 2 == 0 → False,剔除
- 4 % 2 == 0 → True,保留 保留的元素为 0, 2, 4。经
list()转换后得到[0, 2, 4]。
【逐项辨析】 A. [0, 2, 4]:正确。range(5) 产生 0、1、2、3、4,其中 0%2==0、2%2==0、4%2==0 均为 True,被 filter 保留。 B. [1, 3]:错误。这是奇数集合,而 filter 的条件是保留偶数(x % 2 == 0)。 C. [0, 1, 2, 3, 4]:错误。这是未过滤的完整 range(5) 列表。 D. [2, 4]:错误。遗漏了 0。0 虽然是假值(falsy),但 0 % 2 == 0 的布尔结果为 True,因此会被保留。
【知识点】filter(function, iterable) 将函数依次作用于可迭代对象的每个元素,并保留使函数返回真值(truthy)的元素,同样返回一个迭代器(Python 3)。需要特别注意区分“元素本身的真值”与“函数返回值的真值”:filter 依据的是函数的返回值是否为真,而非元素本身是否为真。例如 filter(lambda x: x % 2 == 0, range(5)) 中,虽然 0 是假值元素,但 0 % 2 == 0 返回 True,因此 0 被保留。如果省略函数参数(如 filter(None, [0, 1, False, True])),则直接依据元素本身的真值进行筛选,此时 0 和 False 会被去除。
【记忆锚点】“filter 保真去假看函数,不是看元素本身零不小;零模二得零等于真,千万别把零丢掉。”
【易混对比】
| 用法 | 判断依据 | 0 是否被保留 | False 是否被保留 |
|---|---|---|---|
filter(lambda x: x % 2 == 0, ...) | 函数返回值 | 是(0%2==0 为 True) | 视函数而定 |
filter(None, [0, 1, False, True]) | 元素本身真值 | 否(0 为 falsy) | 否(False 为 falsy) |
换问法: 若题目改为“filter(None, [0, 1, 2]) 的结果是什么?”,答案是 [1, 2],因为 None 表示直接按元素真值筛选,0 被判定为假值而剔除。
【自测】
data = [0, '', 'hello', [], [1, 2], None, True]
print(list(filter(None, data)))答:
['hello', [1, 2], True]。filter(None, ...) 直接依据元素真值过滤,0、空字符串、空列表、None 均为假值,被剔除;非空字符串、非空列表、True 为真值,被保留。 与第 P52 题连考。
【知识关联】
- 同库关联:与 P50;条件过滤。
- 实现层:惰性迭代器。
- 面试追问:① filter(None, L) 含义?② 与推导式 if?
【拓展延伸】
- 变式问法:
filter(lambda x: x>0, L)。 - 版本差异:Py3 惰性。
- 工程注意点:复杂条件用推导式更清晰。
P52 · 知识点:递归
【题目】输出结果是?
def f(n):
return 1 if n <= 1 else n * f(n - 1)
print(f(5))A. 120 B. 15 C. 125 D. 25
答案:A
【考点】递归与阶乘计算。
【结论】选 A。该递归函数计算阶乘,f(5) = 5! = 120。
【推导过程】 函数定义了一个递归求阶乘的逻辑:
- 基准条件:当
n <= 1时,返回 1(0! = 1,1! = 1)。 - 递归条件:当
n > 1时,返回n * f(n - 1)。
代入 n = 5 逐步展开:
f(5) = 5 * f(4)f(4) = 4 * f(3)f(3) = 3 * f(2)f(2) = 2 * f(1)f(1) = 1(触发基准条件)
回代计算:
f(2) = 2 * 1 = 2f(3) = 3 * 2 = 6f(4) = 4 * 6 = 24f(5) = 5 * 24 = 120
【逐项辨析】 A. 120:正确。5 的阶乘为 120。 B. 15:错误。15 是 1+2+3+4+5 的和,而非乘积。 C. 125:错误。125 是 5³ 的结果,不是阶乘。 D. 25:错误。25 是 5² 的结果,不是阶乘。
【知识点】递归是函数直接或间接调用自身的编程技巧,其核心在于两点:递归关系(如何将大问题分解为相似的子问题)和基准条件(终止递归的最简单情形,防止无限循环)。在 Python 中,每次递归调用都会在调用栈中压入一帧,保存局部变量和返回地址。Python 默认的递归深度限制约为 1000(可通过 sys.setrecursionlimit() 调整,但不建议设置过大以免栈溢出)。对于阶乘、斐波那契等数学问题,递归实现直观易懂,但由于函数调用开销和栈深度限制,对于大规模数据通常推荐使用迭代或尾递归优化(Python 未原生支持尾调用优化)。
【记忆锚点】“递归递归,自己调用自己;基准条件别忘了,否则 RecursionError 跑不了。阶乘就是连乘到底,5! 等于 120 要牢记。”
【易混对比】
| 特性 | 递归 | 迭代 |
|---|---|---|
| 实现方式 | 函数自调用 | 循环结构(for / while) |
| 代码可读性 | 高(问题定义直接映射) | 较低(需手动维护状态) |
| 空间复杂度 | 高(调用栈开销) | 低(常数级) |
| 风险 | 栈溢出(RecursionError) | 无限循环 |
| Python 适用性 | 适合深度 < 1000 | 适合大规模数据 |
换问法: 若题目改为“计算 f(1000) 时可能会发生什么?”,答案是有可能抛出 RecursionError,因为 Python 默认递归深度限制约为 1000,1000 层递归可能触及上限。
【自测】 请将以下递归函数改写为迭代版本:
def f(n):
return 1 if n <= 1 else n * f(n - 1)答:
def f_iter(n):
result = 1
for i in range(2, n + 1):
result *= i
return result迭代版本使用循环累积乘积,避免了递归深度限制和函数调用开销。 与第 P51 题连考。
【知识关联】
- 同库关联:与 P41;递归深度与栈。
- 实现层:CPython 调用栈;默认约 1000;可改但受 C 栈限制。
- 面试追问:① 尾递归优化吗(否)?② 如何改迭代?
【拓展延伸】
- 变式问法:阶乘/斐波那契;sys.setrecursionlimit。
- 版本差异:语义稳定。
- 工程注意点:深递归改栈模拟;分治注意深度。