《Python进阶系列》二十六:面试题目:[lambda x: x*i for i in range(4)]

本文详细分析了一道Python面试题,涉及列表推导式与闭包的概念。通过实例解释了为何在列表推导式中使用闭包会导致预期结果与实际输出不符,并提供了解决方案,即通过参数传递确保每个内部函数拥有独立的作用域。同时,文章还介绍了Python的作用域规则,包括LEGB原则,并讨论了如何在局部作用域中修改全局变量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文章已经生成可运行项目,

\quad
\quad

闲着无聊,看了道面试题,瞬间涨姿势了!特地做个总结~

题目

题目如下:

lst = [lambda x: x*i for i in range(4)]
res = [m(2) for m in lst]
print(res)

上述式子的输出结果:

  • 预计结果为:[0, 2, 4, 6]
  • 实际输出为:[6, 6, 6, 6]

why?
\quad

前置知识

首先需要知道匿名函数。匿名函数的关键字为lambda,表现形式为:lambda 参数 : 返回值lambda后面的参数就是函数的形参,冒号后面的表达式就是返回值。

lambda表达式的意义两点:

  1. 对于只有一行的函数,使用此方式可以省去定义函数的过程,使代码简洁明朗;
  2. 对于不需要重复使用之函数,此方式可以在用完之后,立即释放,提高程序执行的性能。

其次,也是重点,需要知道闭包

在Python核心编程里,闭包的定义为:如果在一个内部函数里,对外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认定是闭包。

闭包函数:当前函数引用到上一层函数的局部命名空间的变量时就会触发闭包规则。我们说触发了闭包的函数叫做闭包函数,但是要注意一点:只有当调用闭包函数的时候它才会去引用外层函数的变量,因为在调用闭包函数之前,闭包内部的命名空间还不存在。
\quad

原理

i在外层作用域lambda x: x*i 为内层(嵌)函数,他的命名空间中只有 {'x': 1} 没有i,所以运行时会向外层函数(这里是列表解析式函数[])的命名空间中请求i,而当列表解析式运行时,列表解析式命名空间中的i经过循环依次变化为0-->1-->2-->3最后固定为3,所以当 lambda x: x*i内层函数运行时,去外层函数取i每次都只能取到 3。
\quad

解决办法

解决办法:变闭包作用域为局部作用域。

给内层函数lambda x: x*i增加参数,命名空间中有了用来存储每次的i ,即改成[lambda x, i=i: x*i for i in range(4)]这样每一次,内部循环生成一个Lambda函数时,
都会把i作为默认参数传入Lambda的命名空间。循环4次实际Lambda表达式为:

  • 第一次:lambda x, i=0
  • 第二次:lambda x, i=1
  • 第三次:lambda x, i=2
  • 第四次:lambda x, i=3
>>> fun = [lambda x, i=i: x*i for i in range(4)]
>>> for item in fun:
>>>     print(item(1))
0
1
2
3

\quad

代码论证

首先,[lambda x: x * i for i in range(4)]等价于如下函数:

def func():
    fun_list = []
    for i in range(4):
        def foo(x):
            return x*i
        fun_list.append(foo)
        
    return fun_list

调用函数func(),输出func(),可以看到func()是一个包含四个函数的列表:

>>> func()
[<function __main__.func.<locals>.foo(x)>,
 <function __main__.func.<locals>.foo(x)>,
 <function __main__.func.<locals>.foo(x)>,
 <function __main__.func.<locals>.foo(x)>]

为了论证上面的解释,在函数里打印一些信息,查看该函数命名空间及i值变化:

def func():
    fun_list = []
    for i in range(4):
        def foo(x):
            print('foo函数中 i {} 命名空间为:{}:'.format(i, locals()))
            return x * i
        fun_list.append(foo)
        print('外层函数 i 为:{} 命名空间为:{}'.format(i, locals()))
    return fun_list

当调用函数时:f = func(),会打印如下信息:

>>> f = func()
外层函数 i 为:0 命名空间为:{'fun_list': [<function func.<locals>.foo at 0x00000194AABA8708>], 'foo': <function func.<locals>.foo at 0x00000194AABA8708>, 'i': 0}
外层函数 i 为:1 命名空间为:{'fun_list': [<function func.<locals>.foo at 0x00000194AABA8708>, <function func.<locals>.foo at 0x00000194AAB750D8>], 'foo': <function func.<locals>.foo at 0x00000194AAB750D8>, 'i': 1}
外层函数 i 为:2 命名空间为:{'fun_list': [<function func.<locals>.foo at 0x00000194AABA8708>, <function func.<locals>.foo at 0x00000194AAB750D8>, <function func.<locals>.foo at 0x00000194AABA8798>], 'foo': <function func.<locals>.foo at 0x00000194AABA8798>, 'i': 2}
外层函数 i 为:3 命名空间为:{'fun_list': [<function func.<locals>.foo at 0x00000194AABA8708>, <function func.<locals>.foo at 0x00000194AAB750D8>, <function func.<locals>.foo at 0x00000194AABA8798>, <function func.<locals>.foo at 0x00000194AABA8828>], 'foo': <function func.<locals>.foo at 0x00000194AABA8828>, 'i': 3}

为了排版美观,将输出的函数地址改名为:函数1、2、3。即得到如下输出:

>>> f = func()
外层函数 i 为:0 命名空间为:{'fun_list': [函数1], 'foo': 函数1, 'i': 0}
外层函数 i 为:1 命名空间为:{'fun_list': [函数1, 函数2], 'foo': 函数2, 'i': 1}
外层函数 i 为:2 命名空间为:{'fun_list': [函数1, 函数2, 函数3], 'foo': 函数3, 'i': 2}
外层函数 i 为:3 命名空间为:{'fun_list': [函数1, 函数2, 函数3, 函数4], 'foo': 函数4, 'i': 3}

同时,对f做切片,调用foo()函数之后,可以得到如下输出:

>>> f[0](0), f[0](1), f[0](2), f[0](3)
foo函数中 i 3 命名空间为:{'x': 0, 'i': 3}:
foo函数中 i 3 命名空间为:{'x': 1, 'i': 3}:
foo函数中 i 3 命名空间为:{'x': 2, 'i': 3}:
foo函数中 i 3 命名空间为:{'x': 3, 'i': 3}:

可以看见:就像上面所说的:四次循环中外层函数命名空间中的i0-->1-->2-->3最后固定为3,而在此过程中内嵌函数Lambda函数内因为没有定义i,所以只有Lambda函数动态运行时,在自己命名空间中找不到i才去外层函数复制i = 3过来,结果就是所有Lambda函数的i都为 3,导致得不到预计输出结果:[0, 2, 4, 6]只能得到 [6, 6, 6, 6]

\quad
上面说到了解决办法就是把变闭包作用域为局部作用域。即当在foo()中添加i=i。给内层函数foo()增加默认参数,命名空间中有了用来存储每次的i,这样每一次内部循环生成一个函数时,都会把i作为默认参数传入foo()的命名空间。

def func():
    fun_list = []
    for i in range(4):
        def foo(x, i=i):
            return x * i
        fun_list.append(foo)
    return fun_list

for m in func():
  print(m(2))

这样的话,for循环执行时,就已经把 i(0, 1, 2, 3) 的值传给了foo()函数,此时的i已经是foo()函数的内部变量,运行到foo()函数时,就不会到外部函数寻找变量i,直接运行x * i(0, 1, 2, 3),因此最终结果会是 [0, 2, 4, 6]
\quad

Python的作用域

语言区分作用域,是为了复用变量名。引入作用域,相当于给变量划分了各自的“隔离区”,在不同”隔离区“里,查找变量变得很容易。正是因为有了作用域,我们在函数内才可以随意使用变量名,而不担心其与全局变量、其他函数中的变量冲突。

Python是动态类型语言,变量是在定义的时候赋值的。分以下几个方面来理解:

  • a = 1 赋值时定义变量;
  • from tools import cubie 导入时定义变量cubie
  • def fun():pass 定义函数,绑定变量fun
  • def fun(name=None): pass 定义变量name为函数fun的形式变量(也是局部变量),同时定义函数,绑定变量fun
  • class Car:pass 定义类,绑定类名Car

变量作用域取决于其定义位置。在Python里,只有函数、类、模块会产生作用域,代码块不会产生作用域。作用域按照变量的定义位置可以划分为4类:

  • 局部作用域(Local):定义在函数内部的变量、定义在函数声明中的形式参数;
  • 嵌套作用域(Enclose):定义在函数中,嵌套函数外,且被嵌套函数引用的变量;
  • 全局作用域(Global):定义在 .py 文件内的,且函数、类之外的变量;
  • 内建作用域(Built-in):定义在builtin中的变量。内置作用域是通过一个名为 builtin 的标准模块来实现的,但是这个变量名自身并没有放入内置作用域内,所以必须导入这个文件才能够使用它。

嵌套作用域包含了非局部(non-local)和非全局(non-global)的变量。比如两个嵌套函数,一个函数(或类)A 里面又包含了一个函数 B ,那么对于 B 中的名称来说 A 中的作用域就为 nonlocal。

作用域的查找遵守LEGB规则。即,Python解释器查找变量时,会按照顺序依次查找
局部作用域-->嵌套作用域-->全局作用域-->内建作用域

在任意一个作用域中找到变量则停止查找,所有作用域查找完成没有找到对应的变量,则抛出NameError: name 'xxxx' is not defined的异常。

为了在局部作用域中修改全局变量和自由变量,可以引入global关键字和nonlocal关键字。

\quad

参考

本文章已经生成可运行项目
<think>好的,我现在要解决用户提出的关于Python面试题:[lambda x: i*x for i in range(4)]。首先,我需要理解这个问题到底在问什么。看起来这是一个列表推导式,里面包含了四个lambda函数。用户可能想知道这段代码的输出是什么,或者其中可能存在的陷阱。 首先,我应该回忆一下列表推导式和lambda函数的基本知识。列表推导式会遍历range(4)中的每个i,分别为0,1,2,3,然后为每个i生成一个lambda函数。每个lambda函数的形式都是lambda x: i*x。这时候,我需要考虑变量i的作用域和闭包的问题。 接下来,可能的问题是,当这些lambda函数被创建后,调用它们时,i的值会是多少。因为lambda函数中的i是自由变量,它在运行时才会被解析。这时候,列表推导式中的循环变量i在循环结束后,其值会是最后一个值,也就是3。所以,所有四个lambda函数中的i都会引用这个最终的i值,也就是3。因此,当调用这些lambda函数时,比如f(2),每个函数都会返回3*2=6,而不是预期的0*2, 1*2, 2*2, 3*2。 不过,我需要验证这一点是否正确。比如,实际运行代码,看看结果是否四个函数都返回3*x。例如,执行以下代码: funcs = [lambda x: i*x for i in range(4)] print([f(2) for f in funcs]) 如果结果是[6,6,6,6],那么说明所有lambda函数都使用了i的最终值。这确实是一个常见的Python陷阱,涉及到闭包中变量的延迟绑定。 但为什么会发生这种情况呢?因为在Python中,闭包中的变量是在函数被调用时查找的,而不是在定义时。因此,当lambda函数被创建时,它们并没有捕获当前的i值,而是保留了一个对变量i的引用。当循环结束后,i的值变为3,所以所有函数调用都会使用这个值。 接下来,用户可能想知道如何解决这个问题,也就是让每个lambda函数捕获当前的i值。这时候,可以通过在lambda表达式中将i作为默认参数传递,或者使用立即绑定的方法。例如: funcs = [lambda x, i=i: i*x for i in range(4)] 这样,每个lambda函数在定义时就会捕获当前的i值作为默认参数,因此在调用时使用的i是定义时的值,而不是循环结束后的值。 另外,用户可能希望了解这种现象的原因以及如何避免。这可能涉及到Python的作用域规则和闭包机制。需要解释延迟绑定和早期绑定的区别,以及如何在列表推导式或循环中正确捕获变量。 此外,用户可能想知道这是否是Python的特定行为,还是其他语言也存在类似的问题。但根据当前的问题,可能只需要关注Python的情况。 总结来说,这个面试题考察的是对闭包和作用域的理解,特别是循环中变量绑定的常见陷阱。正确回答需要指出所有lambda函数共享同一个变量i,最终值为3,导致所有函数返回3*x。解决方法是在定义lambda时固定i的值,例如通过默认参数。</think>这是一个关于 Python**闭包(Closure)****变量作用域** 的经典面试题,涉及 `lambda` 表达式在循环中的行为。问题通常表现为以下形式: ```python funcs = [lambda x: i * x for i in range(4)] ``` 当调用 `funcs` 中的函数时(如 `f(2)`),预期输出可能是 `0, 2, 4, 6`,但实际结果会全部是 `6`。以下是详细解释: --- ### **1. 问题分析** #### **代码行为** - 这段代码通过列表推导式生成了 4 个 `lambda` 函数,每个函数接受参数 `x`,并返回 `i * x`。 - **预期**:每个 `lambda` 函数应捕获循环中的当前 `i` 值(0, 1, 2, 3),返回 `0*x`, `1*x`, `2*x`, `3*x`。 - **实际结果**:所有 `lambda` 函数都使用 `i` 的最终值 `3`,因此 `3*x`。 #### **示例验证** ```python funcs = [lambda x: i * x for i in range(4)] print([f(2) for f in funcs]) # 输出 [6, 6, 6, 6],而非预期的 [0, 2, 4, 6] ``` --- ### **2. 原因解释** - **闭包的延迟绑定**Python 中的 `lambda` 函数在定义时不会立即捕获变量 `i` 的值,而是**保留对变量 `i` 的引用**。当 `lambda` 函数被调用时,才会查找 `i` 的值。 - **循环结束后**: `i` 的值在循环结束时为 `3`,因此所有 `lambda` 函数最终都使用 `i=3`。 #### **等效代码** ```python i = 0 lambda x: i * x # 第一个函数 i = 1 lambda x: i * x # 第二个函数 ... i = 3 # 循环结束后,所有函数最终共享 i=3 ``` --- ### **3. 解决方案** 需要让每个 `lambda` 函数捕获循环中当前的 `i` 值,而不是共享最终的 `i`。以下是两种常见方法: #### **(1) 使用默认参数固化当前值** 通过将 `i` 作为默认参数传递给 `lambda`,可以在定义时立即捕获当前值: ```python funcs = [lambda x, i=i: i * x for i in range(4)] print([f(2) for f in funcs]) # 输出 [0, 2, 4, 6] ``` #### **(2) 通过闭包工厂函数** 显式创建闭包,隔离变量作用域: ```python def make_lambda(i): return lambda x: i * x funcs = [make_lambda(i) for i in range(4)] print([f(2) for f in funcs]) # 输出 [0, 2, 4, 6] ``` --- ### **4. 核心知识点** - **闭包的变量查找机制**:闭包中引用的外部变量是“延迟绑定”的,在函数调用时动态查找。 - **循环变量的作用域**:在循环中定义的变量(如 `i`)属于外部作用域,而非每次循环的独立作用域。 - **不可变类型 vs 可变类型**:若 `i` 是可变对象(如列表),修改其内容仍会影响所有闭包。 --- ### **5. 面试回答示例** > **面试**:解释 `[lambda x: i*x for i in range(4)]` 的问题,如何解决? > **回答**: > 这段代码生成的每个 `lambda` 函数共享同一个变量 `i`。由于闭包的延迟绑定,函数在调用时才会查找 `i` 的值,而循环结束后 `i` 的最终值为 3,因此所有函数都会计算 `3*x`。 > 解决方法是让每个 `lambda` 在定义时捕获当前的 `i` 值,例如通过默认参数 `lambda x, i=i: i*x` 或使用闭包工厂函数隔离作用域。 --- 这个问题考察对 Python 作用域和闭包机制的理解,是面试中高频考点。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

奋斗的西瓜瓜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值