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

\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

参考

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

奋斗的西瓜瓜

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

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

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

打赏作者

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

抵扣说明:

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

余额充值