在博文装饰器详解中曾介绍到:带参数的函数装饰器的最外层函数(传入装饰器参数)是一个典型的闭包结构。闭包是众多编程语言中的一个经典结构。按照维基百科的定义:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
上述定义过于书面化,我们由一个例子来引出闭包结构。
results = []
for i in range(4):
def func(x):
return x *i
results.append(func) # 列表元素为函数对象
for f in results:
print(f(2)) # 6 6 6 6
上面的例子中,函数内引用了外部变量i
,因为其无法持久化,所以在实际调用函数时,i
的值均为最后一次循环的值i=3
,从而导致结果均以为6。为了解决上述问题,我们可以采用闭包的方式,将外部变量持久化,其具体做法就是采用类似于装饰器中的嵌套函数的形式:
for i in range(4):
def func(i):
def inner_func(x):
return x * i # 闭包条件1:内层函数应用enclosure变量
return inner_func # 闭包条件1:外层函数返回内层函数对象
results.append(func(i)) # 列表元素为函数对象
for f in results:
print(f(2)) # 0 2 4 6
闭包的格式非常类似于装饰器:均为嵌套函数,外层函数返回内层函数对象。但其又显著区别于装饰器:
(1)从结构上说,装饰器外层函数的参数为被装饰函数;而闭包中外层函数的参数可以为变量(enclose变量),也可以没有变量,但内层函数必须引用来自外部函数作用域的变量;
(2)从功能上说,装饰器的目的是为了为被装饰函数添加新的功能;而闭包的功能是为了内部函数能够持久化外层函数的变量。
闭包必须同时满足如下两个条件:
(1)内层函数应用外层函数enclosure变量
(2)外层函数返回内层函数对象
在对python中变量作用域、命名空间、引用和赋值的理解一文中曾经介绍过嵌套函数的作用域问题:内层函数的local作用域以及嵌套其的外层函数的enclosure作用域。 一般在函数调用结束后,其命名空间删除,但采用闭包的结构,尽管外部函数的命名空间被删除,但定义在其作用域内被内部函数引用的enclosure变量则得以持久化,这也就是闭包结构所以生效的本质。
关于闭包结构,还有几点需要说明的:
(1)闭包中的返回函数(即嵌套结构的内层函数)不要引用任何循环变量,或者后续会发生变化的变量。否则容易出现意想不到的状况(如本文第一个例子)。如果一定要引用循环变量怎么办?方法是再创建一个函数,用该函数的参数绑定循环变量当前的值,无论该循环变量后续如何更改,已绑定到函数参数的值不变(如本文第二个例子)。
(2)闭包无法改变外部函数局部变量指向的内存地址。根据闭包的定义,外部函数局部变量在内部函数中得到持久化,若该变量为可变变量,则可以改变其内引用的值;若为不可变变量,为其同名变量进行赋值操作创造的是一个新的变量。