一道关于Python的笔试题

在一年前学习Python的时候碰到过这么个题目:

1
2
3
4
def mul():
return [lambda x : i*x for i in range(4)]

print([m(2) for m in mul()])

如果用之前学习的语言例如Java来理解的话,这题答案应该是[0,2,4,6]

然而实际运行后,你会发现是[6, 6, 6, 6],很惊讶!通过百度查找后了解到了是因为闭包的原因,导致lambda方法没有在一开始被真正调用,而导致lambda内部不存在引用,只在最后取了i最后一次循环的值——3。然而真的是这样吗?

一年后的时候我又接触到了这个问题,抱着以前似懂非懂的知识+现在的知识 再来理解这个题目的时候,我发现事情可能并不是这样,并且进过查证,印证了我的想法

我假设大部分看这篇文章的同学都已经有了一定的闭包概念,语法上就是一个内层函数使用了外层函数的变量而里面这个函数就是闭包函数,整个逻辑上就是闭包。从上面的解释来说,为什么会出现这个问题是因为闭包的原因。现在我要引出一个概念:

没有块作用域的语言才会有闭包

并且这个题目跟闭包没有关系(或者说没有直接关系),而是块作用域。

Python里的作用域规则是LEGB,Local(函数),Enclosing(外部嵌套函数),Global(全局),Builtin(内置模块),并且查找顺序L->E->G->B

可以发现的是,Python是没有块作用域的,块作用域即类似Java用花括号引起来的代码都是块作用域内的。

1
2
3
for (int i=0; i<10; i++){
System.out.println(i); // i在循环结束后是会被销毁的
}

没有块作用域会导致哪些问题呢?就比如你照常写了个循环代码,在循环体内做了一些函数的绑定逻辑,并且依赖于循环的值i。按理说绑定的函数所用到的参数i在函数体内又被拷贝了一次(由于块作用域的存在,每次循环绑定的i并不会受下一次循环的i影响)。但如果没有块作用域呢?比如没有闭包的情况下的开篇问题:

1
2
3
4
5
lyst = []
for i in range(4):
lyst.append(lambda x: i*x)

print([m(2) for m in lyst])

没有块作用域时,现在这个代码全局就只有一个i!所以列表中的函数都只绑定了i=3的情况,导致最后结果为[6, 6, 6, 6]

有没有发现,上面的代码中并没有闭包,最后的结果和有闭包的情况一致,所以导致这个问题的关键并不是由于闭包引起的,而是Python缺少块作用域

那闭包可以起到了什么作用呢?

因为没有块作用域,则可以用LEGB中的Enclosing—嵌套函数,这也就是闭包的原型,借用外层嵌套函数中的变量来起到缺失的块作用域无法保存块内临时变量的效果,比如改写成这样:

1
2
3
4
5
6
lyst = []
for i in range(4):
lam = lambda i: lyst.append(lambda x: i*x)
lam(i)

print([m(2) for m in lyst]) # 结果:[0, 2, 4, 6]

当然Python中的闭包还可以用做装饰器等特性上,这也是因祸得福的一种设计把= =

所以需要明白两个概念:

  • 是先没有块作用域,而产生的闭包;而不是凭空产生的闭包
  • 不能从结果推过程,这样是不严谨的
觉得好的话就打赏Ta一瓶冰阔落吧