[lambda x: x*i for i in range(4)] 详细解析+LEGB规则 闭包原理

本文深入探讨了Python中使用闭包和lambda表达式时常见的陷阱,解析了为什么预期的输出与实际不符,并提供了修改作用域以解决问题的方法。

转自:http://www.cnblogs.com/shiqi17/p/9608195.html

一、问题描述

fun = [lambda x: x*i for i in range(4)]
for item in fun:
    print(item(1))
    
上述式子的输出结果:
预计结果为:0, 1, 2, 3
实际输出为:3, 3, 3, 3

原理:i 在外层作用域

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

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

给内层函数 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

二、上面看不懂就看这儿
函数fun = [lambda x: x*i for i in range(4)]等价于:如下函数

def func():
    fun_lambda_list = []

    for i in range(4):
        def lambda_(x):
            return x*i
        fun_lambda_list.append(lambda_)
        
    return fun_lambda_list

查看该函数命名空间及 I 值变化:

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

    return fun_lambda_list

fl = func()
fl[0](1)
fl[1](1)
fl[2](1)
fl[3](1)

#运行结果为:
外层函数 I 为:0 命名空间为:{'lambda_': <function func.<locals>.lambda_ at 0x7f7324d04158>, 'fun_lambda_list': [<function func.<locals>.lambda_ at 0x7f7324d04158>], 'i': 0}
外层函数 I 为:1 命名空间为:{'lambda_': <function func.<locals>.lambda_ at 0x7f7324d041e0>, 'fun_lambda_list': [<function func.<locals>.lambda_ at 0x7f7324d04158>, <function func.<locals>.lambda_ at 0x7f7324d041e0>], 'i': 1}
外层函数 I 为:2 命名空间为:{'lambda_': <function func.<locals>.lambda_ at 0x7f7324d04268>, 'fun_lambda_list': [<function func.<locals>.lambda_ at 0x7f7324d04158>, <function func.<locals>.lambda_ at 0x7f7324d041e0>, <function func.<locals>.lambda_ at 0x7f7324d04268>], 'i': 2}
外层函数 I 为:3 命名空间为:{'lambda_': <function func.<locals>.lambda_ at 0x7f7324d042f0>, 'fun_lambda_list': [<function func.<locals>.lambda_ at 0x7f7324d04158>, <function func.<locals>.lambda_ at 0x7f7324d041e0>, <function func.<locals>.lambda_ at 0x7f7324d04268>, <function func.<locals>.lambda_ at 0x7f7324d042f0>], 'i': 3}
Lambda函数中 i 3 命名空间为:{'x': 2, 'i': 3}:
Lambda函数中 i 3 命名空间为:{'x': 2, 'i': 3}:
Lambda函数中 i 3 命名空间为:{'x': 2, 'i': 3}:
Lambda函数中 i 3 命名空间为:{'x': 2, 'i': 3}:

为了排版美观,我已将输出lambda_函数地址改名为:lam函数1 2 3

外层函数I为:0 命名空间为:{'i': 0, 'lambda_': lam函数1 'fun_lambda_list': [lam函数1]}
外I:1 命空:{'i': 1, 'lambda_': lam函数2, 'fun_lambda_list': [lam函数1, lam函数2]}
外I:2 命空:{'i': 2, 'lambda_': lam函数3, 'fun_lambda_list': [lam函数1, lam函数2, lam函数3]}
外I:3 命空:{'i': 3, 'lambda_': lam函数4, 'fun_lambda_list': [lam函数1, lam函数2, lam函数3, lam函数4]}
Lambda函数中 i 3 命名空间为:{'i': 3, 'x': 1}:
Lambda函数中 i 3 命名空间为:{'i': 3, 'x': 1}:
Lambda函数中 i 3 命名空间为:{'i': 3, 'x': 1}:
Lambda函数中 i 3 命名空间为:{'i': 3, 'x': 1}:

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

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

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

fl = func()
res = []

res.append(fl[0](1))
res.append(fl[1](1))
res.append(fl[2](1))
res.append(fl[3](1))

print(res)

#输出结果为:
Lambda函数中 i 0 命名空间为:{'x': 1, 'i': 0}:
Lambda函数中 i 1 命名空间为:{'x': 1, 'i': 1}:
Lambda函数中 i 2 命名空间为:{'x': 1, 'i': 2}:
Lambda函数中 i 3 命名空间为:{'x': 1, 'i': 3}:
[0, 1, 2, 3]

给内层函数 lambda_增加默认参数,命名空间中有了用来存储每次的 i , 即改成 def lambda_(x, i=i) : 这样每一次,
内部循环生成一个lambda 函数时,都会把 i 作为默认参数传入lambda的命名空间
循环4次实际lambda表达式为:
第一次:lambda_( x, i=0) 第二次:lambda_(x, i=1) 第三次:lambda_(x, i=2) 第四次:lambda_(x, i=3)

这样我们就能得到预计的结果:0, 1, 2, 3

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

Local(函数内部)局部作用域

Enclosing(嵌套函数的外层函数内部)嵌套作用域(闭包)

Global(模块全局)全局作用域

Built-in(内建)内建作用域

python解释器查找变量时,会按照顺序依次查找局部作用域—>嵌套作用域—>全局作用域—>内建作用域,在任意一个作用域中找到变量则停止查找,所有作用域查找完成没有找到对应的变量,则抛出 NameError: name ‘xxxx’ is not defined的异常。

人生还有意义。那一定是还在找存在的理由

<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 作用域和闭包机制的理解,是面试中高频考点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值