关于闭包的解释,引用一下维基百科的解释吧:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
wtf?这是什么鬼解释,不说人话,看不懂,也不敢问。
没关系,一大段话看不懂,那我们就来一句一句的看:
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。
这句话中我们不用关心名字,知道了原理后,我们自然知道为什么它叫这个名字。
我们需要理解的,应该是‘自由变量’这四个字。
举个例子吧:
a = 0
def fun():
a += 1
print(a)
fun()
print(a)
F:\Python\python.exe F:/Project/untitled/mytest.py
0
Traceback (most recent call last):
File "F:/Project/untitled/mytest.py", line 9, in <module>
fun()
File "F:/Project/untitled/mytest.py", line 5, in fun
a += 1
UnboundLocalError: local variable 'a' referenced before assignment
这是啥意思呢,它是说a这个本地变量还没绑定好值呢,你就去引用a,自然就报错了。
有的小伙伴可能会问了,我不是在代码最开头就绑定了a = 0吗,为什么说没有绑定呢
要讲清楚这个问题,我们先回到Python的变量作用域LEGB来。
这里是引用
B —— Builtin(Python);Python内置模块的命名空间 (内建作用域) (内置命名空间)
G —— Global(module); 函数外部所在的命名空间 (全局作用域) (全局命名空间)
E —— Enclosing function locals;外部嵌套函数的作用域(嵌套作用域) (局部命名空间)
L —— Local(function);当前函数内的作用域 (局部作用域) (局部命名空间)
观察刚才的代码,我们最开始的’a = 0’这句代码,实际上是声明了一个全局变量,即LEGB中的G(Global),然后在函数fun中,'a += 1’这句代码,我们希望的是为a加上1,这个操作希望修改的是全局变量a。
那为什么解释器抛出的错误是:local variable ‘a’ 在绑定前被引用呢?
原因是这样的:a += 1 并不是一个原子操作,它等价于a = a + 1,这句代码被解释器执行的时候,被分解为了三个步骤。
- 先看等号左边,即我们声明一个变量a,a的值尚未绑定(如果类比C语言来描述,就是声明了一个int型变量a,计算机为该变量a分配了内存,但此时内存中没有任何东西。)
- 再看等号右边, 找到变量a,给它加个1。
- 最后是等号,将a + 1之后的值,绑定给 a。
那么问题来了,执行到第二步的时候,解释器尝试寻找变量a,这个时候函数fun中已经声明了一个a变量,即local a已经被声明了,解释器自然就不会再去找最开始我们定义的那个global a了。
找到local a之后,给a加个1,这时候就出问题了,local a只是被声明了,但还没有任何实际值,怎么能给它加1呢?
于是乎,解释器便抛出了UnboundLocalError这个错误。
理清楚了这个问题之后,那就对症下药吧,Python提供了global这个关键字来解决上述问题。
a = 0
def fun():
# 用global关键字给a加上之后,解释器就知道找变量a的时候去全局作用域找
# 而不是在本地作用域找了,修改之后的值,同样绑定到了全局作用域中的a
global a
a += 1
fun()
print(a) # 1
此时,在函数fun中经过global关键字声明的a,就成为了‘自由变量’,这个‘自由变量’是相对而言的,对于函数fun外的‘a = 0’,a是一个全局变量,对于函数fun内,a不是在本地作用域绑定的,那么a就是一个自由变量。
接下来,我们继续看维基百科闭包释义中的第二句:
这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。
上面的那个例子中,函数是fun,被引用的自由变量是a,但并未离开创造它的环境,那么请看以下例子:
对装饰器概念还不清楚的小伙伴,可以看一下我的另一篇博客https://blog.csdn.net/weixin_43498178/article/details/101857141
def countit(fun):
"""
这是一个装饰器
统计被装饰函数执行了多少次
"""
count = 0
def inner(*args, **kwargs): # inner是个闭包
# nonlocal的作用区别于global但又是类似的
# 它声明这个count存在于外部嵌套函数的作用域
nonlocal count
count += 1
res = fun(*args, **kwargs)
print('This function:%s was called %s times' % (fun.__name__, count))
return res
return inner
@countit
def fun():
pass
@countit
def func():
pass
for _ in range(3):
fun()
for _ in range(3):
# 虽然fun中的count此时已经变成了3
# 但func中的count仍是从0开始
func()
This function:fun was called 1 times
This function:fun was called 2 times
This function:fun was called 3 times
This function:func was called 1 times
This function:func was called 2 times
This function:func was called 3 times
观察以上代码,inner函数中存在自由变量count,countit函数返回引用了自由变量的函数inner,通过装饰器的形式,这个inner函数(闭包)绑定给了fun,使得fun成为了一个新的函数。
根据维基百科中的释义,此时的fun已经离开了创造它的环境,但fun中的自由变量a仍然和fun是一同存在的,以上代码的输出也印证了这个结果。
看维基百科中释义的最后两句话:
所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
分别运行fun和func三次,得到的结果,两个count之间是相互独立的,因此,我们认为fun和它引用的count,func和它引用的count,这两者有各自的引用,他们是不同的实体,他们虽然都是由countit创建出来的闭包,但因为具备不同的引用环境(自由变量count名字相同,实际引用却不同),即使他们的函数是同一个函数inner,此二者却是不同的实例。