谈谈闭包

我第一次听说“闭包”这个概念是在学习Lua的时候。由于此前并没有接触到函数式编程的语言,所以满脑子C/C++的思维方式的我被“闭包”困惑了很久。我找到了一个比较通俗的闭包定义:如果在一个内部函数里,对在外部作用域(但不是在全局作用域)的变量进行引用,那么内部函数就被认为是闭包(closure)。Lua的学习资料上一般都会有一个这样关于闭包的例子:

function new_counter()  
    local n = 0 
    local function counter()        
        n = n + 1       
        return n    
    end 
    return counter
end 

c1 = new_counter()
c2 = new_counter()
print(c1())  //打印1 
print(c2())  //打印2

首先,new_counter()是一个返回函数的函数;然后这个被返回的counter函数会更改其外部函数(new_counter)的局部变量,并返回计数值。注意counter()函数是在new_counter()函数执行完毕之后被调用的(这好像是废话!),而counter()函数的作用就是读写变量n,所以要理解这个例子的关键在于这个变量n。
熟悉C/C++运行机制的同学都应该知道,C/C++的局部变量是存在于堆栈上的。一个函数执行完毕后,栈顶指针要退回,所以局部变量就消失了。然而支持闭包的语言Lua,Python等等的局部变量的存放方式却不一样,这基本上也是一句废话(这些动态语言都不直接操作硬件堆栈),但是记住这个结论就好——局部变量也是可以长期有效的(在函数返回之后)。而内部函数——闭包(可返回到更外层的作用域)就可以在随后的时间里执行,即操作外层函数(如new_counter函数)的局部变量。就这样,数据成为了函数的附庸。
然后说说Python的闭包吧!其实写这个东西的念头就是来自于看《Python源码剖析》时遇到的一些问题。还是先看看这个Python版的计数器吧!

def new_counter():  
    n = 0   
    def counter():      
        n = n + 1       
        return n    
    return counter

非常不好意思,这例子是不能运行的。Python会报错:“local variable ‘n’ referenced before assignment”。这是因为Python默认变量为局部变量,所以在第四行,它认为n是一个局部变量,并且是一个没有被初始化的局部变量,这自然是要报错了。Python规定变量的作用域要遵守“LEGB”的规则,即按照Local作用域——直接外围作用域——全局作用域——builtin作用域的顺序去寻找变量的定义。然而事实上,Python倒在了L到E的路上。在非全局变量的情况下,Python2的定义和赋值是一起的,没有什么关键字和语法说明n是直接外部作用域的变量,所以这段程序是无法运行的。有一种用list实现闭包赋值的方法,如下

def new_counter():  
    n = [0] 
    def counter():      
        n[0] = n[0] + 1     
        return n[0] 
    return counter

这种方法的关键就在于第四行的“n[0]”只能是赋值,这中语法使得定义和赋值能够区分开来。而且Python3中增加了nonlocal关键字,这个关键字明确的指示编译器到外部寻找变量,这才是真正的遵守了LEGB规则。
仔细一想,好像支持闭包的语言好像都是动态语言,当然Go是个例外。我猜想Go语言中,函数的局部变量应该是在堆空间中的,相当于都是new出来的。支持闭包的另一个关键点是垃圾收集,否者局部变量的管理会成为一个问题。还有就是要支持函数嵌套定义(又是废话了)。

发表评论