0%

Python - 闭包

1. 闭包的概念

闭包(closure):
内部函数中对enclosing作用域中的变量进行引用,这个内部函数就称为闭包。

关于enclosing作用域相关的知识可以参考>>传送门 - Python函数作用域<<

2. 举例解释

首先要理解python中的函数也是一个对象,它是有属性和返回值的,而在函数执行完后其内部变量会被解释器回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# G
pass_line = 60

def func(score):
# E
print("%x" % id(score)) # 以16进制打印score的id值
pass_line = 90
result = "yes" if score >= pass_line else "no"
print(result)

def inner_func(): # score在E作用域中,inner_func就是个闭包, 有__closure__属性
# L
print(score)
print(inner_func.__closure__)
return inner_func

f = func(80) # 语句1 func()返回了一个函数对象inner_func
f() # 语句2 此时f即inner_func
print(f.__closure__)
1
2
3
4
5
6
运行结果:
6e7e6ac0
no
(<cell at 0x000001FC0B9AB6A8: int object at 0x000000006E7E6AC0>,)
80
(<cell at 0x0000019CDFBDB6A8: int object at 0x000000006E7E6AC0>,)

两个本来语句1运行完之后,func内部的变量都已经被回收了,那么这个 80为什么还可以被语句2打印出来呢?
注意下这个现象f.__closure__这个元组的第一个元素的id和score变量的id值相同。
说明如果内部函数中对enclosing作用域中的变量进行引用之后,这个变量就会被添加到inner_func的__closure__属性这个元组中,这样因为有了这个添加到元组的引用,score变量就不会被回收了,再调用f函数时,就会在它的__closure__属性中查找score变量,这样就自然而然的可以引用了。

理解了这个例子就可以搞懂Python的闭包了。


3. 一个简单的应用

下面的代码就是应用闭包达到了自定pass_line的目的

1
2
3
4
5
6
7
8
9
10
11
def func(pass_line):
def inner_func(score): # score在E作用域中,inner_func就是个闭包, 有__closure__属性
result = "yes" if score >= pass_line else "no" # 此时引用了enclosing作用域中的pass_line,那么pass_line变量就被添加到了__closure__属性中
print(result)
return inner_func

f = func(90) # 语句1 func()返回了一个函数对象inner_func
f(80) # 语句2 此时f即inner_func

f2 = func(60)
f2(80)
1
2
3
运行结果:
no
yes

4. 经典错误代码

参考>>传送门 - 闭包经典错误代码<<

1
2
3
4
5
6
7
8
9
def foo():
a = 1
def bar():
a = a + 1
return a
return bar

c = foo()
c()
1
2
会报错如下:
local variable 'a' referenced before assignment

这是因为在执行代码 c = foo()时,python会导入全部的闭包函数体bar()来分析其的局部变量,
python规则指定所有在赋值语句左面的变量都是局部变量,
则在闭包bar()中,变量a在赋值符号”=”的左面,被python认为是bar()中的局部变量。
再接下来执行print c()时,程序运行至a = a + 1时,因为先前已经把a归为bar()中的局部变量,
所以python会在bar()中去找在赋值语句右面的a的值,结果找不到,就会报错。解决的方法很简单:

1
2
3
4
5
6
def foo():
a = [1]
def bar():
a[0] = a[0] + 1
return a[0]
return bar

只要将a设定为一个容器就可以了。这样使用起来多少有点不爽,
所以在python3以后,在a = a + 1 之前,使用语句nonloacal a就可以了,该语句显式的指定a不是闭包的局部变量。