看一下这道题:
def fun():
temp = [lambda x: i * x for i in range(4)]
return temp
for every_lambda in fun():
print(every_lambda(2))
猜猜以上代码输出什么?
初看这道题时,我几乎是脱口而出,这不就是0,2,4,6嘛,so easy。
然而…
6
6
6
6
wtf?简直是6到不行,啪啪打脸。
直接看lambda和列表推导式,或许不大直观,我将它改了一下:
def fun():
inners = []
for i in range(4):
def inner(x):
return i * x
inners.append(inner)
return inners
for inner in fun():
print(inner(2))
这么来看就清晰直观多了,fun中有个循环,循环生成了4个闭包,每个闭包都引用了自由变量i,然后返回这4个闭包构成的列表。
初看这道题时,我以为inner的自由变量i是直接绑定了嵌套作用域(即for中的i)的值,但分析后发现,inner中的i并不是直接绑定好值的,而是延迟绑定(late binding)!
即只有在inner被调用的时候,inner才会去嵌套作用域查找i的值!
而以上代码中inner被调用时,for循环已经结束,整个嵌套作用域,只存在一个i,它的值是3
知道问题所在之后,我恍然大悟,看来闭包的这个延迟绑定的特性,的确有点坑人啊。
那么,如果我如何才能让整个坑人的闭包,返回我真正想要的0,2,4,6呢?
这里提供两种方式:
方式1:设置默认参数
def fun():
inners = []
for i in range(4):
def inner(x, i=i): # 使用默认参数的方式,将for循环取出的i立即绑定到默认参数上
return i * x
inners.append(inner)
return inners
这种方式看似没有改变代码结构,但实际上inner已经不是闭包了.
inner函数体中的变量i并不是自由变量,不会去外部作用域中查找,它就是自身参数定义中已经设置好的默认参数。
Python中一切皆对象,函数也是一个对象,它拥有__default__属性,返回的是默认参数构成的元组,拥有__closure__属性,返回的是闭包所引用的自由变量。
def fun():
inners = []
for i in range(4):
def inner(x, i=i): # 使用默认参数的方式,将for循环取出的i立即绑定到默认参数上
return i * x
inners.append(inner)
return inners
for inner in fun():
print(inner.__defaults__, inner.__closure__)
print(inner(2))
(0,) None
0
(1,) None
2
(2,) None
4
(3,) None
6
很明显,__closure__返回的是None,说明此时inner已经不是闭包了。
方式二:生成器
def fun():
for i in range(4):
def inner(x):
return i * x
yield inner # 由之前的一次性返回4个闭包,改为一次返回1个闭包
for inner in fun():
print(inner.__closure__)
print(inner(2))
(<cell at 0x00000138CBEF65E8: int object at 0x0000000061E3BBF0>,)
0
(<cell at 0x00000138CBEF65E8: int object at 0x0000000061E3BC10>,)
2
(<cell at 0x00000138CBEF65E8: int object at 0x0000000061E3BC30>,)
4
(<cell at 0x00000138CBEF65E8: int object at 0x0000000061E3BC50>,)
6
但方式二存在一个问题,当用list构造函数先行将生成器构造好一个列表,然后再调用列表中的闭包,结果仍是6,6,6,6
def fun():
for i in range(4):
def inner(x):
return i * x
yield inner # 由之前的一次性返回4个闭包,改为一次返回1个闭包
inners = list(fun())
for inner in inners:
print(inner(2))
6
6
6
6
说了这么多,总结下来,闭包的延迟绑定,就是只在闭包被调用时,才会去寻找闭包所引用的自由变量。