\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
表达式的意义两点:
- 对于只有一行的函数,使用此方式可以省去定义函数的过程,使代码简洁明朗;
- 对于不需要重复使用之函数,此方式可以在用完之后,立即释放,提高程序执行的性能。
其次,也是重点,需要知道闭包。
在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}:
可以看见:就像上面所说的:四次循环中外层函数命名空间中的i
从 0-->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