程序语言的闭包
闭包的概念、形式与应用
什么是闭包?
闭包并不是什么新奇的概念,它早在高级语言开始发展的年代就产生了。闭包(Closure)是词法闭包(Lexical Closure)的简称。闭包是由函数及其相关的引用环境组合而成的实体(即:闭包=函数+引用环境)。
函数只是一段可执行代码,编译后就"固化"了,每个函数在内存中只有一份实例,得到函数的入口便可以执行函数。在函数式编程语言中,函数是一阶值,函数可以作为另一个函数的参数或返回值,可以赋给一个变量。函数可以欠嵌套定义,即在一个函数内部可以定义另一个函数,有了嵌套函数这种结构,便会产生闭包问题。如:
def ExFunc(n):
sum=n
def InsFunc():
return sum+1
return InsFunc
>>myFunc=ExFunc(10)
>>myFunc()
11
>>myAnotherFunc=ExFunc(20)
>>myAnotherFunc()
21
>>myFunc()
11
>>myAnotherFunc()
21
在这段程序中,函数InsFunc是函数ExFunc的内嵌函数,并且是ExFunc函数的返回值。我们注意到一个问题:内嵌函数InFunc中引用到外层函数中的局部变量sum, Python会怎么处理这个问题呢?先让我们来看看这段代码的运行结果。当我们调用分别由不同的参数调用ExFunc函数得到的函数时(myFunc(),myAnotherFunc()),得到的结果是隔离的,也就是说每次调用ExFunc函数后都将生成并保存一个新的局部比变量sum。其实这里ExFunc函数返回的就是闭包。
引用环境
按照命令式语言的规则,ExFunc函数只是返回了内嵌函数InsFunc的地址,在执行InsFunc函数时将会由于在其作用域内找到不到sum变量而出错。而在函数式语言中,当内嵌函数体内引用到体外的变量时,将会把定义时涉及到的引用环境和函数体打包成一个整体(闭包)返回。现在给出引用环境的定义就容易理解了:应用环境是指在程序执行中的某个点所有处于活跃状态的约束(一个变量的名字和其所代表的对象之间的联系)所组成的集合。闭包的使用和正常的函数调用没有区别。
由于闭包把函数和运行时的引用环境打包成为一个新的整体,所以就解决了函数编程中的嵌套问题所引发的问题。如上述代码段中,当每次调用ExFunc函数时都将返回一个新的闭包实例,这些实例之间是隔离的,分别包含调用时不同的引用环境现场。不同于函数,闭包的运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
闭包只是在形式和表现上像函数,但实际上不是函数。函数是一些可执行代码,这些代码在函数被定以后就确定了,不会在执行时发生变化,所以一个函数只有一个实例。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。所谓引用环境是指在程序执行中的某个点所有处于活跃状态的约束所组成的集合。其中的约束是指一个变量的名字和其所代表的对象之间的联系。那么为什么要引用环境与函数组合起来呢?这主要是因为在支持嵌套作用域的语言中,有时不能简单直接地确定函数的引用环境。这样的语言一般具有这样的特性:
-
函数是一阶值(First-class value),即函数可以作为另一个函数的返回值或参数,还可以作为一个变量的值。
-
函数可以嵌套定义,即在一个函数内部可以定义另一个函数。
考虑如下代码
function make_counter()
local count=0
function inc_count()
count=count+1
return count
end
return inc_count
end
c1=make_counter()
c2=make_counter()
print(c1())
print(c2())
在这段程序中,函数inc_count定义在函数make_counter内部,并作为make_counter的返回值。变量count不是inc_count内的局部变量,按照最内嵌套作用域的规则,inc_count中的count引用的是最外层函数中的局部变量count。接下来的代码中两次调用make_counter()
这里存在一个问题,当调用make_counter时,在其执行上下文中生成了局部变量cout的实例,所以函数inc_count中的count引用的就是这个实例。但是inc_count并没有在此时被执行,而是作为返回值返回。当make_counter返回后,其执行上下文失效,count实例的生命周期也就结束了,在后面对c1和c2调用实际是对inc_count的调用,而此处并不在count的作用域中,这看起来是无法正确执行的。
在这样的语言中,如果按照作用域规则在执行时确定一个函数的引用环境,那么这个引用环境可能和函数定义时不同。要想使这两段程序正常执行,一个简单的办法是在函数定义时捕获当时的引用环境,并与函数代码组合成一个整体。当把这个整个当做函数调用时,先把其中的引用环境覆盖到当前的引用环境上,然后执行具体代码,并在调用结束后恢复原来的引用环境。这样就保证了函数定义和执行时的引用环境是相同的。这种由环境与函数代码组成的实体就是闭包。当然如果编译器或解释器能够确定一个函数在定义和运行时的引用环境是相同的(一个函数中没有自由变量时,引用环境不会发生变化),那就没有必要把引用环境和代码组合起来了,这时只需要传递普通的函数就可以了。现在可以得出这样的结论:闭包不是函数,只是行为和函数相似,不是所有被传递的函数都需要转化为闭包,只有引用环境可能发生变化的函数才需要这样做。
一个编程语言需要哪些特性来支持闭包呢,下面列出一些比较重要的条件:
函数是一阶值
函数是可以嵌套定义
可以捕获引用环境,并把引用环境和函数代码组成一个可调用的实体
允许定义匿名函数
这些不是必要条件,但具备这些条件能说明一个编程对闭包的支持较为完善。
借用一个非常好的说法来做个总结:对象是附有行为的数据,而闭包是附有数据的行为。
闭包的表现形式
Python中的闭包
Python因其简单易学,功能强大而拥有很多拥护者,很多企业和组织在使用这种语言。Python使用缩进来区分作用域的做法也十分有特点。下面是一个python的例子:
def addx(x):
def adder(y): return x+y
return adder
add8=addx(8)
add9=addx(9)
print add8(100)
print add9(100)
在python中使用def来定义函数时,是必须有名字的,要想使用匿名函数,则需要使用lambda语句,向下面的代码这样:
def addx(x):
return lambda y: x+y
add8=addx(8)
add9=addx(9)
print add8(100)
print add9(100)
闭包的应用
闭包可以用优雅的方式来处理一些棘手的问题,有些程序员声称没有闭包简直就活不下去了。这虽然有些夸张,却从侧面说明闭包有着强大的功能。
加强模块化
闭包有利于模块化编程,它能以简单的方式开发较小的模块,从而提高开发速度和程序的可复用性。和没有使用闭包的程序相比,使用闭包可将模块划分得更小。比如我们要计算一个数组中所有数字的和,这只需要循环遍历数组,把遍历到的数字加起来就行了。如果现在要计算所有元素的积呢?要打印所有的元素呢?解决这些问题都要对数组进行遍历,如果是在不支持闭包的语言中,我们不得不一次又一次重复写循环语句。而在支持闭包的语言中识是不必要的。
抽象
闭包是数据和行为的组合,这使得闭包具有较好抽象能力。