为什么会遇到 闭包、匿名函数这几个知识点?
最近学习python基础, 碰到一道代码题:
def func1():
return [lambda x:i*x for i in range(4)]
print([m(2) for m in func1()])
其正确输出为什么? 正确答案为
:
[
6
,
6
,
6
,
6
]
:[6,6,6,6]
:[6,6,6,6]
相比你能看到这个博客,很大概率遇到了类似的题目。
我们先说说包含哪些知识点:
- 匿名函数 :
lambda x:i*x
中的 l a m b d a lambda lambda关键字 - 闭包函数 :
lambda x:i*x
中调用了上一层函数for i in range(4)
的局部命名空间的变量的 i i i - for循环对函数的迭代调用 :匿名函数部分以及
[m(2) for m in func1()]
我们一步一步拆分理解:
一、for循环对函数的迭代调用
代码运行时,首先运行的便是print([m(2) for m in func1])
这表示对func1
返回的列表进行遍历操作,具体要看其返回的是什么。
在调试Debug时, 也是运行到这后,进入先进入func1
中去获取其返回。
二、匿名函数且触发闭包
def func1():
return [lambda x:i*x for i in range(4)]
将上面函数简单化为:
def func1():
list_i =[]
for i in range(4):
res = lambda x:x*i //匿名函数
list_i.append(res)
return list_i
可以看到首先出现了匿名函数,
匿名函数:python 不通过def来声明函数名字,而是通过lambda关键字来定义的函数称为匿名函数。 好处:
- 因为函数没有名字,不必担心函数名冲突。
- 匿名函数也是一个函数对象,也可以把匿名函数赋值给一个变量,再利用变量来调用该函数。
- 封装细节,提高安全性和可控性,(经常在全局作用域中被用于函数外部,从而限制向全局作用域中添加过多的变量和函数)。
- 在全局作用域中使用块级作用域可以减少闭包占用内存的问题,(因为没有指向匿名函数的引用,只要函数执行完毕,就可以立即销毁其作用域链了)。
具体可以参考Python函数学习——匿名函数
Python 匿名函数详解
进一步看, 匿名函数调用了func1
(上一级函数)中的局部变量 i
, 这就出发了闭包
def func1():
list_i =[]
for i in range(4):
res = lambda x:x*i //匿名函数
list_i.append(res)
return list_i
闭包的可以参考: Python技法,闭包
一开始,func1只是要返回一个列表, 真正调用到匿名函数的是m(2)去遍历这个列表时才进行操作, 也就是这一原因导致输出与想象的不同。
代码运行的具体流程
我们用以下代码查看,验证:
def func1():
list_i =[]
for i in range(4):
def lambda_(x):
print('等效匿名函数中 i {} 命名空间为:{}:'.format(i, locals()))
res = i*x
return res
list_i.append(lambda_)
print('变量 i 为:{} 命名空间为:{}'.format(i, locals()))
return list_i
# 第一步执行逻辑,打印空间地址
list_a = func1()
print(list_a)
#代入数据计算
ans=[]
for m in list_a:
res = m(2)
print("m = {} \t res={}".format(m, res))
ans.append(res)
#打印最终结果
print(ans)
其输出结果为:
变量 i 为:0 命名空间为:{
'list_i': [<function func1.<locals>.lambda_ at 0x000002222B22A040>],
'lambda_': <function func1.<locals>.lambda_ at 0x000002222B22A040>,
'i': 0}
变量 i 为:1 命名空间为:{
'list_i': [<function func1.<locals>.lambda_ at 0x000002222B22A040>,
<function func1.<locals>.lambda_ at 0x000002222B22A0D0>],
'lambda_': <function func1.<locals>.lambda_ at 0x000002222B22A0D0>,
'i': 1}
变量 i 为:2 命名空间为:{
'list_i': [<function func1.<locals>.lambda_ at 0x000002222B22A040>,
<function func1.<locals>.lambda_ at 0x000002222B22A0D0>,
<function func1.<locals>.lambda_ at 0x000002222B22A160>],
'lambda_': <function func1.<locals>.lambda_ at 0x000002222B22A160>,
'i': 2}
变量 i 为:3 命名空间为:{
'list_i': [<function func1.<locals>.lambda_ at 0x000002222B22A040>,
<function func1.<locals>.lambda_ at 0x000002222B22A0D0>,
<function func1.<locals>.lambda_ at 0x000002222B22A160>,
<function func1.<locals>.lambda_ at 0x000002222B22A1F0>],
'lambda_': <function func1.<locals>.lambda_ at 0x000002222B22A1F0>,
'i': 3}
[ <function func1.<locals>.lambda_ at 0x000002222B22A040>,
<function func1.<locals>.lambda_ at 0x000002222B22A0D0>,
<function func1.<locals>.lambda_ at 0x000002222B22A160>,
<function func1.<locals>.lambda_ at 0x000002222B22A1F0>]
等效匿名函数中 i 3 命名空间为:{'x': 2, 'i': 3}:
m = <function func1.<locals>.lambda_ at 0x000002222B22A040> res=6
等效匿名函数中 i 3 命名空间为:{'x': 2, 'i': 3}:
m = <function func1.<locals>.lambda_ at 0x000002222B22A0D0> res=6
等效匿名函数中 i 3 命名空间为:{'x': 2, 'i': 3}:
m = <function func1.<locals>.lambda_ at 0x000002222B22A160> res=6
等效匿名函数中 i 3 命名空间为:{'x': 2, 'i': 3}:
m = <function func1.<locals>.lambda_ at 0x000002222B22A1F0> res=6
[6, 6, 6, 6]
其操作步骤如下图所示:
关于是否为闭包的问题
我自己认为, 按定义,本题应该是触发了闭包。
- 与闭包无关
也有博主 [ a l o a l o l o l ] ^{[aloalolol]} [aloalolol]的文章分析将func1去掉证明不是闭包,但是我感觉他的改动依然触发了闭包,因为调用了外层函数的变量,即便外层函数是指主函数的语句。
他的博文如下: aloalololo-与闭包无关
这篇博文写的很好,我自己也是通过这篇文章厘清了为什么会是【6,6,6,6】
但我不太认同跟闭包没关系,比方说例子:
def func(x):
return x +i
ans= []
for i in range(4):
res = func(2)
print("i=",i,"res=",res)
ans.append(res)
print(ans)
输出:
i= 0 res= 2
i= 1 res= 3
i= 2 res= 4
i= 3 res= 5
[2, 3, 4, 5]
ans= []
for i in range(4):
def func(x):
return x +i
ans.append(func(2))
print(ans)
输出:
[2, 3, 4, 5]
并没有像本次遇到的顺序问题,所以我以为应该是闭包原因。
- 与闭包有关
我比较倾向,博主 [ 裸睡的雨 ] ^{[裸睡的雨]} [裸睡的雨]的分析, 他的博文如下:裸睡的雨[m(2) for m in multipliers()]-------面试题
这里跟我自己的运行打印的结果比较相符,本文也是参考其博文整理的。
这位博主中引用了几个比较好的帖子, 这里也放一下:
fun = [lambda x: x*i for i in range(4)] 本质解析/原理,LEGB规则 闭包原理
总结
以上就是我的学习和理解路径,虽然我现在也不敢完全肯定说是闭包的原因,但是至少下次遇到这类题,不至于抓瞎,博主 [ 裸睡的雨 ] ^{[裸睡的雨]} [裸睡的雨]帖子下有一些讨论, 大家可以看一下, 如果有哪位大佬有正确答案,希望可以@我一下,让我也学习一下。