闭包

函数闭包的概念在初学时容易迷糊,尤其涉及到闭包的作用域和参数问题。先看下维基百科对闭包的定义。

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

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

Python中的闭包

函数(内层函数)的定义嵌套在另一个函数(外层函数)之中,内层函数引用了外层函数的自由变量。调用外层函数时,运行到的内层函数def语句仅仅是完成对内层函数的定义,而不会去调用内层函数,除非在嵌套函数之后又显式的对其进行调用。

简单来说,一个闭包就是你调用了一个函数A,这个函数A返回了一个函数B给你。这个返回的函数B就叫做闭包。
闭包函数须满足以下条件:
1.在函数内部定义函数;
2.包含对外部作用域而非全局作用域的自由变量的引用;

自由变量(free variable),是一个技术术语,指未在本地作用域中绑定的变量。例如下面例子中的x,对于内部函数f2来说,x没在本地作用域f2中进行绑定,x是自由变量。

例如:

1
2
3
4
5
6
7
8
9
x = 99
def f1():
    x = 88
    def f2():
        print(x)
    return f2  # 返回内部函数

a = f1()  
a()  # 结果 88

可以看出,f1中的嵌套变量x覆盖了全局变量x=99,然后f2中的本地变量按照引用规则,就引用了x=88。闭包的特殊之处在于,执行a()的时候,自由变量x = 88的作用域已经消失,但是还是能读取到x的值。

当然,你也可以在显示调用内部函数。

例如:

1
2
3
4
5
6
7
8
9
x = 99
def f1():
    x = 88
    def f2():
        print(x)
    f2() # 显式调用内部函数。

#执行函数f1
f1()  # 结果 88

下面我们来说说嵌套作用域的一个特殊之处:
本地作用域在函数结束后就立即失效,而嵌套作用域在嵌套的函数返回后却仍然有效。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def f1():
    x = 88
    def f2():
        print(x)
    return f2

#执行f1函数后,返回函数对象f2,再将f2赋值给action变量。action变为函数对象
action = f1()
#执行函数action
action() # 结果 88

函数f1中定义了函数f2,f2引用了f1嵌套作用域内的变量x,并且f1将函数f2作为返回对象进行返回。最值得注意的是我们通过变量action获取了返回的f2,虽然此时f1函数已经退出结束了,但是f2仍然记住了f1嵌套作用域内的变量名x。一个能记住嵌套作用域变量值的函数,尽管作用域已经不存在。

闭包的一个应用就是工厂函数。
工厂函数定义了一个外部的函数,这个函数简单的生成并返回一个内嵌的函数,仅仅是返回却不调用,因此通过调用这个工厂函数,可以得到内嵌函数的一个引用。

例如:

1
2
3
4
5
6
7
8
def maker(n):
    k = 8
    def action(x):
        return x ** n + k
    return action

f = maker(2)
print(f) #结果 <function maker.<locals>.action at 0x00000000021C51E0>

再看一个例子:

1
2
3
4
5
6
7
8
def maker(n):
    k = 8
    def action(x):
        return x ** n + k
    return action

f = maker(2)
print(f(4)) # 结果 24

这里我们可以看出,内嵌的函数action记住了嵌套作用域内得两个嵌套变量,一个是变量k,一个是参数n,即使后面maker返回并退出。我们通过调用外部的函数maker,得到内嵌的函数action的引用。这种函数嵌套的方法在装饰器中会经常用到。这种嵌套作用域引用,就是python的函数能够保留状态信息的主要方法了。

nonlocal语句

本地函数通过global声明对全局变量进行引用修改,那么对应的,内嵌函数内部想对嵌套作用域中的变量进行修改,就要使用nonlocal进行声明。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def test(num):
    in_num = num
    def nested(label):
        nonlocal in_num
        in_num += 1
        print(label, in_num)
    return nested

F = test(0)
F('a')
F('b')
F('c')

执行以上程序会输出如下结果:

1
2
3
a 1
b 2
c 3

我们在nested函数中通过nonlocal关键字引用了内嵌作用域中的变量in_num,那么我们就可以在nested函数中修改他,即使test函数已经退出调用,这个“记忆”依然有效。 再一个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def test(num):
    in_num = num
    def nested(label):
        nonlocal in_num
        in_num += 1
        print(label, in_num)
    return nested

F = test(0)
F('a')
F('b')
F('c')
G = test(100)
G('mm')

执行以上程序会输出如下结果:

1
2
3
4
a 1
b 2
c 3
mm 101

多次调用工厂函数返回的不同内嵌函数副本F和G,彼此间的内嵌变量in_num是彼此独立隔离的。


转载请注明本网址。