Python--闭包

Python--闭包

写这个博客还得起源于潜水快一个学期的老班在班群里提的一个问题:

1
2
3
4
5
6
7
functions = []
for i in range(5):
    def func(x):
        return x + i
    functions.append(func)
for f in functions:
    print(f(12))

输出结果都是16,原因?

没有运行前我觉得老班说错了(丢人现场),一运行人都傻了,这是为什么呢,可以猜到作为一个全局变量 i,在函数 func 中一定只是作为一个全局的符号存在,而不是一个临时变量,同时返回的 x+i 中 i 的绑定并不是在函数创建的时候就绑定好的,它仅仅在用的时候才会去绑定值,这个时候 i 为 4。

似乎这么就结束了?老班提起闭包和延迟绑定……

闭包(Closure)

基本理解

很多语言都有闭包这一概念,我们单看 python 的,闭包概念可以这么理解:

  • 如果我们在一个函数的内部定义了另一个函数,那么我们称外部的函数为外函数,内部的为内函数。
  • 在一个外函数中定义了一个函数,内函数里运用外函数的临时变量,并且外函数的返回值是内函数的引用,这样就构成了一个闭包。

一般情况下,如果一个函数结束,函数的内部所有东西都会释放掉,还给内存,局部变量都会消失。但是闭包是一种特殊情况,如果外函数在结束的时候发现有自己的临时变量将来会在内部函数中用到,就把这个临时变量绑定给了内部函数,然后自己再结束。

我们稍稍改造一下上面的例子就能得到一个标准的闭包形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def outer(n):
functions = []
for i in range(n):
def func(x):
return x + i

functions.append(func)
return functions


fs = outer(5)
for f in fs:
print(f)
print(f(12))
  • 外部函数返回了内部函数的引用:对于每个列表里的元素,都是一个函数指针的引用,他们的地址都不相同,f 就是这个指针的引用,所以我们可以通过 f 调用 func() 函数

  • 外部函数把临时变量绑定给了内部函数:和上面的例子不同了,这次 i 作为一个临时变量存在了(但相对于 func 依旧为全局变量),这时 outer 函数执行完后,理论上 i 应该会被释放啊,但那是通常的情况,闭包是另外的一种情况。外部函数发现自己的临时变量会在将来执行的内部函数中发挥作用,那么自己在结束的时候会将外函数的临时变量送给内函数来绑定,这样不至于执行内函数的时候得到一个 undefined name 的报错。

那这是不是意味对于outer传入不同的参数,最终返回的都是同一个内部函数的引用?

上面的例子看着不太方便,我们换一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
def outer(n):

def func(x):
return x + n
return func
f1 = outer(5)
f2 = outer(7)
print(f1)
print(f2)

#
# <function outer.<locals>.func at 0x000001D2A2BE86A8>
# <function outer.<locals>.func at 0x000001D2B1C79950>

明显的看到,每次都创建了一个新的内部函数

  • python中一切都是对象,虽然函数我们只定义了一次,但是外函数在运行的时候,实际上是按照里面代码执行的,外函数里创建了一个函数,我们每次调用外函数,它都创建一个内函数,虽然代码一样,但是却创建了不同的对象,并且把每次传入的临时变量数值绑定给内函数,再把内函数引用返回。虽然内函数代码是一样的,但其实,我们每次调用外函数,都返回不同的实例对象的引用,他们的功能是一样的,但是它们实际上不是同一个函数对象。

修改外函数的临时变量

我们如果想修改外函数变量的值,就碰到了一个问题

1
2
3
4
5
6
7
8
9
def outer(n):
b = 10
def func():
b += 1
print(b)
return func
f1 = outer(5)
f1()
# UnboundLocalError: local variable 'b' referenced before assignment

啊,这。

在基本的 Python 语法中,一个函数可以随意的读取全局的变量,但要修改全局变量就需要声明为 global 了,同样,在修改闭包的变量的时候,我们也需要关键字来辅助我们。

使用关键字 nonlocal 声明 一个变量, 表示这个变量不是局部变量空间的变量,需要向上一层变量空间找这个变量。

修改后的上述代码:

1
2
3
4
5
6
7
8
9
def outer(n):
b = 10
def func():
nonlocal b
b += 1
print(b)
return func
f1 = outer(5)
f1()

正常的输出了 b 的值

还有一点需要注意:使用闭包的过程中,一旦外函数被调用一次返回了内函数的引用,虽然每次调用内函数,是开启一个函数执行过后消亡,但是闭包变量实际上只有一个,每次开启内函数都在使用同一份闭包变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def outer(n):
def func(y):
nonlocal n
n += y
print(n)
return func


f1 = outer(5)
f1(3)
f1(3)
# 结果
# 8
# 11

延迟绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# code 1
functions = []
for i in range(5):
    def func(x):
        return x + i
    functions.append(func)
for f in functions:
    print(f(12))

# code 2
def outer(n):
functions = []
for i in range(n):
def func(x):
return x + i

functions.append(func)
return functions


fs = outer(5)
for f in fs:
print(f)
print(f(12))

2 个看起来很相似,但还是有点区别的,个人认为:

通过 dis 模块的 dis 函数来查看 bytecode,我们先看 outer 函数的

code2
1
2
3
4
9           0 LOAD_FAST                0 (x)
2 LOAD_DEREF 0 (i)
4 BINARY_ADD
6 RETURN_VALUE

如果对整个函数进行反编译的话可以看到 LOAD_CLOSURE 加载闭包,直接对应的 i 的一步操作

我们仅仅看内部函数 i 作为一个引用出现

code1
1
2
3
4
5
9           0 LOAD_FAST                0 (x)
2 LOAD_GLOBAL 0 (i)
4 BINARY_ADD
6 RETURN_VALUE

对于 code1,i 作为一个全局变量出现,这里我认为仅仅是函数的延迟绑定发挥作用,而和闭包的关系不大

Author

Ctwo

Posted on

2020-05-24

Updated on

2020-10-25

Licensed under

Comments