闭包
闭包(Closure)是Python中的高级概念,它只出现在嵌套函数中,但和嵌套函数有很大的不同,那就是闭包使用了一个叫自由变量的东西。
实际中,闭包就是一个函数,只不过这个函数可以引用没有在该函数内部定义的变量,这个函数本身可以是匿名函数也可以是非匿名函数。
光说不练假把式,下面通过一个例子理解一下。
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
print("series in averager: ", series)
total = sum(series)
return total / len(series)
return averager
上面定义了一个函数 make_averager
,这个函数的返回值是一个函数对象 averager,谨记“Python中一切皆可对象”的真理。当每次 averager 被调用的时候,它就会接收一个新的值,并把这个值添加到 series 数组中,并计算此时数组的平均值。这个函数到底有什么威力呢?再来看下面的使用例子。
avg = make_averager()
print(avg(10))
print(avg(11))
输出结果如下:
series in averager: [10]
10.0
series in averager: [10, 11]
10.5
看出端倪了吗? 通过调用 avg(n)
就可以实现更新历史 series 并且计算当前 series 的平均值。那么到底是如何实现更新 series 呢?讲道理,series 是 make_averager
函数中定义的一个局部变量,并且当通过 avg(n)
调用时,明显 make_averager
已经被执行过了,应该就访问不到 series 变量了。
实际上在嵌套函数 averager
中,series 是一个自由变量(free variable),表明这个变量没有被绑定在局部作用域中,同时 averager
函数的闭包行为使得它可以访问到这个自由变量。
这里的自由变量 series 保存在函数的只读属性 __closure__
中,我们可以打印看一下:
print(avg.__closure__)
(<cell at 0x11822a8e8: list object at 0x1166541c8>,)
print("####:",avg.__closure__[0].cell_contents)
####: [10, 11]
可以看到 __closure__
存储的是一个 list 对象,通过第二个 print 也可以看到当前 series 中的两个元素。
如果这个函数仅仅是嵌套函数,那么它的 __closure__
应该是 None。
nonlocal
上面代码中的 series 是可变变量,在内部函数中使用时没有问题,但如果是不可变变量,如 string、number、tuple 等,在内部函数引用则会报错。还是上面的例子,将它稍微改写一下,变成下面的代码,好处是不用每次都重新计算 series 的 sum。
def make_averager():
total = 0
count = 0
def averager(new_value):
count += 1
total += new_value
return total / count
return averager
avg = make_averager()
print(avg(10))
这时运行代码则会报错,报错信息如下:
UnboundLocalError Traceback (most recent call last)
<ipython-input-323-5c1ff34d8622> in <module>
9
10 avg = make_averager()
---> 11 print(avg(10))
12
13 print(avg.__closure__)
<ipython-input-323-5c1ff34d8622> in averager(new_value)
3 count = 0
4 def averager(new_value):
----> 5 count += 1
6 total += new_value
7 return total / count
UnboundLocalError: local variable 'count' referenced before assignment
问题出在 count+=1
这行代码,为什么呢?因为在Python中并没有要求先声明一个变量,所以Python解释器任务在函数体内,只要对一个变量进行赋值操作,那么这个变量就是局部变量。而 count+=1
相当于 count=count+1
,对 count 进行了赋值操作,所以Python解释器认为 count 是函数内的局部变量,但是从执行顺序上来说,count+1
则是先执行的,先引用了局部变量 count,即使外面有一个同名的 count 变量。并且此时 count 就不再是自由变量了,也不会存储在 __closure__
中。
那么如何解决这个问题呢?Python3中引入了 nonlocal
关键字,它可以使得一个被赋值的局部变量变为自由变量,并且 nonlocal
声明的变量发生变化时,__closure__
中存储的值也会发生变化。所以只要对 count 及 total 变量加上 nonlocal
的声明就可以了,即:
def make_averager():
total = 0
count = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
avg = make_averager()
print(avg(10))
print("####:",avg.__closure__[0].cell_contents, avg.__closure__[1].cell_contents)
print(avg(11))
print("####:",avg.__closure__[0].cell_contents, avg.__closure__[1].cell_contents)
运行结果为:
10.0
####: 1 10
10.5
####: 2 21
打印 __closure__
的值,也可以发现确实发生了变化。