当一个函数中嵌套定义了一个内层函数,且该内层函数引用了外层函数的变量,那么该内层函数就是一个闭包,如下所示。
def outer():
x = 1
y = 2
def inner():
print(x)
return inner
f1 = outer()
f1() # out --> 1
怎么理解闭包(Closure)这个词?闭包指的是这样一种对象:将某个对象和对象所在的部分环境信息打包在一起,形成一个新的对象,这个包含了原对象所在的执行环境信息的对象就是闭包。在python中,闭包中的原对象指的是原始内层函数,原对象所在的环境信息指的就是变量x, 新的对象(闭包)就是代码中的内层函数inner,原始inner函数在底层实现,对用户隐藏;所以outer函数最后返回的inner函数就是一个闭包。此时,我们也称outer函数为工厂函数,或者更准确的称为闭包工厂函数(生产闭包的工厂函数)。
由于闭包会保存所在执行环境的信息,上例中,闭包inner保存了outer的本地变量x的引用,所以尽管outer函数执行结束返回inner闭包给f1,f1依然可以正常运行,因为其已经保存了x的引用。
对于本地命名空间,当函数退出后,在内存中便不会存在;虽然x属于outer函数的本地命名空间,但是由于嵌套函数引用了x,实际上在底层实现上,x和y并不属于同一个结构体(即使属于同一个命名空间,但由于命名空间是一个抽象概念,不对应具体实体,所以并不矛盾),outer结束后,y所属的结构体会被消除,但是x所属的结构体依然会保留,所以其中的对象也会在内存中继续保留。
我们可以通过python的dis模块解析outer函数,通过解释器执行的字节码来证实,变量x和y属于不同结构体。如下所示:
dis.dis(outer)
2 0 LOAD_CONST 1 (1)
2 STORE_DEREF 0 (x)
3 4 LOAD_CONST 2 (2)
6 STORE_FAST 0 (y)
4 8 LOAD_CLOSURE 0 (x)
10 BUILD_TUPLE 1
12 LOAD_CONST 3 (<code object inner at 0x0000021E53973870, file "<ipython-input-104-67473ff04f11>", line 4>)
14 LOAD_CONST 4 ('outer.<locals>.inner')
16 MAKE_FUNCTION 8 (closure)
18 STORE_FAST 1 (inner)
6 20 LOAD_FAST 1 (inner)
22 RETURN_VALUE
Disassembly of <code object inner at 0x0000021E53973870, file "<ipython-input-104-67473ff04f11>", line 4>:
5 0 LOAD_GLOBAL 0 (print)
2 LOAD_DEREF 0 (x)
4 CALL_FUNCTION 1
6 POP_TOP
8 LOAD_CONST 0 (None)
10 RETURN_VALUE
可以看到,对应代码x=1的字节码是block 2,其中通过STORE_DEREF指令将变量x存储起来;对比变量y,对应的是block 3,对变量y的存储指令是STORE_FAST,两种不同的存储指令对应不同结构体;存储一般的本地变量到本地命名空间就是通过STORE_FAST,而存储这种被闭包引用的本地变量,也叫闭包的自由变量,通过STORE_DEREF指令。
最后再看一个例子,该例子既可以加深对闭包的理解,也可以当作一个小陷阱来学习。下面的例子中,for循环中将匿名函数放进了一个列表中并返回,可能有的人会误认为输出结果是[0,1,2,3,4,5,6,7,8,9],但是返回的列表中的函数输出的结果都是一样的,全部都是9。这是因为函数只有在调用的时候才会进行变量值的获取,在outer返回的闭包列表中,每个闭包保存的i的值是一样的,都是最后一个循环下i的值,因此每个闭包的调用执行结果都是一样的。
def outer(n):
funcs = []
for i in range(n):
func_tmp = lambda : i
funcs.append(func_tmp)
return funcs
funcs = outer(10)
print([f() for f in funcs])
# out --> [9, 9, 9, 9, 9, 9, 9, 9, 9, 9]
那么如果我们想得到[0,1,2,3,4,5,6,7,8,9]这样的结果应该怎么做呢?一种做法是,将每次循环下i的值作为参数传入,这样由于lambda语句和def一样是可执行的,每次执行的时候都会保存一份当前参数值引用的副本,从而每次循环下匿名函数参数对应的是不同值的引用,从而执行的时候便会得到不同的结果,具体如下所示。
def outer(n):
funcs = []
for i in range(n):
func_tmp = lambda x=i: x
funcs.append(func_tmp)
return funcs
funcs = outer(10)
print([f() for f in funcs])
# out --> [0,1,2,3,4,5,6,7,8,9]