Python 闭包

在了解闭包之前,我们先来看Python里变量的作用域

(一)变量作用域

在下面的示例中,我们定义一个函数,包含两个变量a和b,b在函数里没有定义

>>> def f1(a):
...     print(a)
...     print(b)
... 
>>> f1(1)
1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f1
NameError: name 'b' is not defined

b没有定义,出现以上的错误并不奇怪

如果我们先给全局变量b赋值,再调用f1呢?

>>> b = 6
>>> f1(1)
1
6

可见程序并没有报错,可以正确的打印b的值

我们再看一个示例:

>>> def f2(a):
...     print(a)
...     print(b)
...     b = 9
... 
>>> b = 6
>>> f2(1)
1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in f2
UnboundLocalError: local variable 'b' referenced before assignment

结果很让人惊讶,既然全局变量b已经赋值为6了,为什么还会报错呢?

事实是,Python编译函数的定义体时,它判断b是局部变量,因为在函数中给b赋值了。这不是缺陷,而是Python设计选择,Python不要求声明变量,但是假定在函数定义中赋值的变量是局部变量

如果在函数中为变量赋值,又想让解释器把b当成全局变量,要使用global声明:

>>> def f3(a):
...     print(a)
...     global b
...     print(b)
...     b = 9
... 
>>> b = 6
>>> f3(1)
1
6

 

(二)闭包

闭包的定义:闭包是指延伸了作用域的函数,其中包含函数定义体中引用,但不在定义体中定义的非全局变量

定义好复杂,我们还是通过示例来理解

假如我们需要一个函数,它的作用是计算不断增加的系列值的均值,例如,整个历史中某个商品的平均收盘价(每天都会增加新价格,平均值要考虑到目前为止所有的价格)

函数使用效果如下:

>>> avg(1)
1.0
>>> avg(2)
1.5
>>> avg(3)
2.0

初学者可能会像下面的示例这样使用类实现:

>>> class Averager():
...     def __init__(self):
...             self.series = []
...     def __call__(self, new_value):
...             self.series.append(new_value)
...             total = sum(self.series)
...             return total/len(self.series)
... 
>>> avg = Averager()

接下来的示例我们使用函数实现(高阶函数):

>>> def make_averager():
...     series = []
...     def averager(new_value):
...             series.append(new_value)
...             total = sum(series)
...             return total/len(series)
...     return averager
... 

调用make_averager返回一个averager函数对象,我们看到和之前类实现有同样的效果:

>>> avg = make_averager()
>>> avg(1)
1.0
>>> avg(2)
1.5
>>> avg(3)
2.0

对于这个示例,函数如何运行的呢?series是在make_averager()函数中定义的局部变量,调用avg(1)时,函数make_averager已经返回,而他的本地作用域也不存在了

事实上,series叫做自由变量(free variable),指未在本地作用域中绑定的变量,如下图

图中closure就是我们所说的闭包(闭包是指延伸了作用域的函数,其中包含函数定义体中引用,但不在定义体中定义的非全局变量)。

我们在控制台省察下make_averager()创建的函数对象

>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)

可以看到avg.__code__.co_freevars里保存了自由变量series

那series的历史值呢?在这里:

>>> avg.__closure__[0].cell_contents
[1, 2, 3]

综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然自由变量的定义作用域不可用了,但是仍能使用哪些绑定

需要注意的是:只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量

 

 (三)nonlocal变量

但是对于make_averager的实现,我们把历史值保存在列表里,每次计算平均值都需要重新求和,当历史值较多时,需要占用比较多的空间并且效率也不高。

更好的实现方法是只存储和sum及元素个数count:

>>> def make_averager():
...     count = 0
...     sum = 0
...     def averager(new_value):
...             count += 1
...             sum += new_value
...             return sum/count
...     return averager
... 
>>> avg = make_averager()

但是上面的实现是有缺陷的:

当执行count += 1运算时,相当与执行count = count + 1, 这会把count变为局部变量,更新变量的值时python会尝试重新绑定,sum变量也是一样的。

因此程序执行时会报错如下:

>>> avg(1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in averager
UnboundLocalError: local variable 'count' referenced before assignment

为了解决这个问题,python引入了nonlocal变量

把上面的实现稍做修改:

>>> def make_averager():
...     count = 0
...     sum = 0
...     def averager(new_value):
...             nonlocal count, sum
...             count += 1
...             sum += new_value
...             return sum/count
...     return averager

再次执行程序:

>>> avg = make_averager()
>>> avg(1)
1.0
>>> avg(2)
1.5
>>> avg(3)
2.0

 

关于闭包,就到这里了

posted on 2017-10-27 15:42  _Joshua  阅读(235)  评论(0编辑  收藏  举报

导航