Skip to content

五、函数与作用域(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单行简单表达式、临时使用无
其他语言函数functionJavaScript、C 等视语言而定

换问法: 若题目改为“Python 中定义匿名函数使用的关键字是?”,则应选择 D(lambda)。

【自测】 以下代码的输出是什么?

python
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/APython 实际机制

换问法: 若题目改为“执行 def f(a): a.append(1) 后外部列表是否会改变?”,答案是会改变,因为 list 是可变对象且 append 属于原地修改。

【自测】

python
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 · 知识点:可变默认参数 ​

【题目】连续调用的输出结果是?

python
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 作为占位符,在函数体内部按需创建新可变对象:

python
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 = []。

【自测】

python
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}) 时星号的作用是什么?”,答案是在调用端进行解包,将元组和字典展开为独立参数。

【自测】 以下代码输出什么?

python
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)。

【自测】 以下哪种调用方式是合法的?

python
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 · 知识点:作用域 ​

【题目】输出结果是?

python
x = 10
def f():
    x = 5
f()
print(x)

A. 10 B. 5 C. 抛异常 D. None

答案:A

【考点】函数内的赋值创建局部变量,不影响全局变量。

【结论】选 A。函数内部的赋值操作创建局部变量,不影响同名的全局变量。

【推导过程】

  1. 全局作用域中执行 x = 10,创建全局变量 x,绑定到整数对象 10。
  2. 定义函数 f,其内部执行 x = 5。由于该语句对 x 进行了赋值操作,Python 编译时将 x 标记为 f 的局部变量。
  3. 调用 f() 时,函数内部创建局部变量 x 并绑定到 5,此操作完全在局部命名空间中进行,与全局 x 无关。
  4. 函数返回后,全局变量 x 仍然指向 10。
  5. 执行 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 后再赋值。

【自测】

python
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 ​

【题目】输出结果是?

python
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。

【推导过程】

  1. 全局作用域执行 x = 1,全局变量 x 绑定到整数 1。
  2. 函数 f 内部第一行声明 global x,明确告知 Python:f 内对 x 的所有操作均作用于全局变量。
  3. 执行 x += 1,即 x = x + 1。由于 global 声明,读取的 x 是全局变量 1,加 1 后赋值给全局 x,全局 x 更新为 2。
  4. 调用 f() 后,全局 x 的值已变为 2。
  5. 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;异常类型不变,别因文案不同以为换了异常。

【自测】

python
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()() 会?

python
def outer():
    n = 1
    def inner():
        n += 1
        return n
    return inner

A. 返回 2 B. 抛出 UnboundLocalError(修改外层变量需 nonlocal 声明) C. 返回 1 D. 返回 None

答案:B

【考点】闭包内修改外层函数变量必须用 nonlocal。

【结论】选 B。内层函数中修改外层变量但未声明 nonlocal,会抛出 UnboundLocalError。

【推导过程】

  1. 调用 outer() 时,创建局部变量 n = 1。
  2. 定义内层函数 inner。由于 inner 引用了外层变量 n,理论上形成了闭包。
  3. inner 内部执行 n += 1。该语句等价于 n = n + 1,包含对 n 的读取和赋值。
  4. Python 编译 inner 时,发现函数体内有对 n 的赋值操作,因此将 n 判定为 inner 的局部变量。
  5. 运行时执行 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 的子类)。
  6. 若要在 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。

【自测】

python
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 里最常见。”

【易混对比】

特性lambdadef 定义的函数
是否有名称无(匿名)有(绑定到函数名)
函数体内容单个表达式任意多行语句
是否包含 return表达式结果自动返回需要显式 return
适用场景简单临时逻辑复杂、复用性高的逻辑

换问法: 若题目改为“lambda 函数体中可以包含多条语句吗?”,答案是不可以,lambda 只能包含单个表达式。

【自测】 将以下 lambda 转换为等价的 def 函数:

python
max_key = lambda d: max(d, key=d.get)

答:

python
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 对象(一种迭代器),而非列表。

【自测】 以下两段代码结果是否相同?

python
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 被判定为假值而剔除。

【自测】

python
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 · 知识点:递归 ​

【题目】输出结果是?

python
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 逐步展开:

  1. f(5) = 5 * f(4)
  2. f(4) = 4 * f(3)
  3. f(3) = 3 * f(2)
  4. f(2) = 2 * f(1)
  5. f(1) = 1(触发基准条件)

回代计算:

  • f(2) = 2 * 1 = 2
  • f(3) = 3 * 2 = 6
  • f(4) = 4 * 6 = 24
  • f(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 层递归可能触及上限。

【自测】 请将以下递归函数改写为迭代版本:

python
def f(n):
    return 1 if n <= 1 else n * f(n - 1)

答:

python
def f_iter(n):
    result = 1
    for i in range(2, n + 1):
        result *= i
    return result

迭代版本使用循环累积乘积,避免了递归深度限制和函数调用开销。 与第 P51 题连考。

【知识关联】

  • 同库关联:与 P41;递归深度与栈。
  • 实现层:CPython 调用栈;默认约 1000;可改但受 C 栈限制。
  • 面试追问:① 尾递归优化吗(否)?② 如何改迭代?

【拓展延伸】

  • 变式问法:阶乘/斐波那契;sys.setrecursionlimit。
  • 版本差异:语义稳定。
  • 工程注意点:深递归改栈模拟;分治注意深度。

持续学习,持续积累。