跳转至

3.3.闭包

闭包的概念

以下引用自维基百科中关于闭包的介绍:

计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

闭包的概念出现于60年代,最早实现闭包的程序语言是Scheme。之后,闭包被广泛使用于函数式编程语言如ML语言LISP。很多命令式程序语言也开始支持闭包。

在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。其中所引用的变量称作上值(upvalue)。


维基百科中对于闭包的介绍,应该还是比较直观的。简而言之:内嵌函数引用了外部函数的变量,这个内嵌函数被执行时,就形成一个闭包。例如:

def outside(a, b):
    a = a
    b = b

    def inner():
        y = a + b
        print(locals())

    inner()


# 调用outside函数 inner被执行 形成闭包
outside(1, 2)
{'y': 3, 'a': 1, 'b': 2}

inner()被执行时,创建的局部命名空间会包括其引用的外层函数的变量,这样的局部命名空间被称为闭包命名空间(enclosing namespace)。

注意,在Python中,内嵌函数可以被外层函数返回,也就是Python函数可以返回闭包。 理解什么是闭包不难,但问题在于闭包有什么作用呢?

闭包的作用

闭包的独特之处在于它可以绑定外部函数的变量,即使生成闭包的环境(外层函数)已经释放,闭包仍然存在。

这个过程很像类生成实例,不同的是外部函数只在调用时生成命名空间,执行完毕后其命名空间就会释放,而类的命名空间在读入定义时创建,一般Python解释器退出才释放命名空间。因此对一些需要重用的功能且不足以定义为类的行为,使用闭包会比使用类占用更少的资源,且更轻巧灵活。

例如:假设我们仅仅想打印出各类动物的叫声,分别以类和闭包来实现:

# 类实现
class Animal(object):
    def __init__(self, animal):
        self.animal = animal

    def sound(self, voice):
        print(self.animal, ':', voice, "...")

dog = Animal("dog")
dog.sound("wangwang")
dog.sound("wowo")
dog : wangwang ...
dog : wowo ...
# 闭包实现
def voice(animal):
    def sound(voc):
        print(animal, ':', voc, "...")
    return sound

dog = voice("dog")
dog("wangwang")
dog("wowo")
dog : wangwang ...
dog : wowo ...

输出结果是完全一样的,但显然类的实现相对繁琐,且这里只是想输出一下动物的叫声,定义一个Animal类未免小题大做,而且voice函数在执行完毕后,其命名空间就已经释放,但Animal类及其实例dog的相应属性却一直贮存在内存中。而这种内存占用是没有必要的。

除此之外,闭包还有其他作用。闭包可以减少函数参数的数目,因此可以用于封装。这对并行计算也非常有用,比如可以让每台电脑负责一个函数的计算。另外,闭包在Python中有一种重要的应用——装饰器。这个会在装饰器的文章讲述。

闭包作用域与命名空间

来看一个典型的闭包结构:

gv = ['a', 'global', 'var']

def func(v):
    gv = ['gv'] + gv  # UnboundLocalError:local variable 'gv' referenced before assignment
    lv = []

    def inn_func():
        lv = lv + [v] # UnboundLocalError:local variable 'lv' referenced before assignment
        gv.insert(1, lv[0])
        return gv

    return inn_func

这段代码似乎没有问题,赋值操作从右到左执行,先从右边的gvlv开始执行代码,此时局部作用域还没有gv变量,局部作用域也没有lv变量,Python应该会访问外层作用域的gvlv变量。但实际调用func()函数时,上面两处对 gvlv 进行赋值操作的地方都会触发 UnboundLocalError

这是因为 Python 在执行函数前,会首先生成各层命名空间和作用域,因此 Python 在执行赋值前会将func 内的 gvlv 写入局部命名空间和闭包命名空间:

dict_局部命名空间.update({"gv":['gv'] + gv})
dict_闭包命名空间.update({"lv":['lv'] + lv})

当 Python 执行赋值时,按照LEGB搜索规则,会先在局部作用域、闭包作用域内发现gvlv 标识符,但gvlv 在局部命名空间和闭包命名空间内都没有绑定对象,从而引发错误。

这段代码本意只是想让具有对象的全局变量gv和局部变量lv参与运算,而不是局部命名空间中的gv和闭包命名空间中的lv。为了避免类似的情况发生,Python 引入了 globalnonlocal 语句来说明所修饰的 gvlv 分别来自全局命名空间和局部命名空间,声明之后,就可以在 funcinn_func 内直接改写上层命名空间内 gvlv 的值:

gv = ['a', 'global', 'var']
print("gv的内存地址", id(gv))


def func(v):
    global gv
    print("gv的内存地址", id(gv))
    gv = ['gv'] + gv
    print("gv的内存地址", id(gv))
    lv = []
    print("lv的内存地址", id(lv))

    def inn_func():
        nonlocal lv
        print("lv的内存地址", id(lv))
        lv = lv + [v]
        gv.insert(1, lv[0])
        return gv

    return inn_func

a = func('is')
a()
print("gv的内存地址", id(gv))
gv的内存地址 2632865909568
gv的内存地址 2632865909568
gv的内存地址 2632865938560
lv的内存地址 2632865909568
lv的内存地址 2632865909568
gv的内存地址 2632865938560

如上,全局变量 gv 值被函数改写了, inn_func 修改的也确实是父函数 lv的值(依据内存地址判断)。

借壳

那么是不是不使用 globalnonlocal 就不能达到上面的目的呢?来看看下面这段代码:

gv = ['a', 'global', 'var']

def func(v):
    gv.insert(0, 'gv')
    lv = []
    print("lv的内存地址", id(lv))

    def inn_func():
        lv.append(v)
        print("lv的内存地址", id(lv))
        gv.insert(1, lv[0])
        return gv

    return inn_func

a = func('is')
a()
print(gv)
lv的内存地址 2632865851840
lv的内存地址 2632865851840
['gv', 'is', 'a', 'global', 'var']

可以发现,执行结果同上面完全一致,问题自然来了:为什么不用 global nonlocal 也可以改写全局变量gv和父函数变量lv的值?

为了看清楚这个过程,我们将上面的gv.insert(0, 'gv')lv.append(v) 改写为 gv[0:0] = ['gv']lv[:] = [v]

gv = ['a', 'global', 'var']

def func(v):
    gv[0:0] = ['gv']
    lv = []
    print("lv的内存地址", id(lv))

    def inn_func():
        lv[:] = [v]
        print("lv的内存地址", id(lv))
        gv.insert(1, lv[0])
        return gv

    return inn_func

a = func('is')
a()
print(gv)
lv的内存地址 2632865937024
lv的内存地址 2632865937024
['gv', 'is', 'a', 'global', 'var']

执行结果完全一致,事实上两者之间的本质也是完全一样的。.insert().append()方法并没有修改 gvlv,而是修改 gvlv 的元素 gv[0:0]lv[:] ,因此gvlv并没有被加入局部命名空间。因此,不需要 globalnonlocal修饰就可以直接改写,这就是“借壳”。另外,也是借助了list对象的mutable性质。

nonlocal 尚未引入 Python 中,比如 Python 2.x 若要在子函数中改写父函数变量的值就得通过这种方法。