闭包
人们有时会把闭包和匿名函数弄混。这是有历史原因的:在函数内部定义函数不常见,直到开始使用匿名函数才会这样做。而且,只有涉及嵌套函数时才有闭包问题。
闭包指延伸了作用域的函数,其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。
下面通过示例来理解
假如有个名为avg的函数,它的作用是计算不断增加的系列值的均值;例如,整个历史中某个商品的平均收盘价。每天都会增加新价格,因此平均值要考虑至目前为止所有的价格。
起初,avg是这样使用的:
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
初学者可能会使用类来实现,如下示例1:
#示例1:计算移动平均值的类
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)
Averager的实例是可调用对象:
>>> avg = Averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
函数式实现,使用高阶函数make_averager,如下示例2:
#示例2:计算移动平均值的高阶函数
def make_averager():
series = []
def averager(new_value):
series.append(new_value)
total = sum(series)
return total/len(series)
return averager
调用make_averager时,返回一个averager函数对象。每次调用averager时,它会把参数添加到系列值中,然后计算当前平均值,如下:
>>> avg = make_averager()
>>> avg(10)
10.0
>>> avg(11)
10.5
>>> avg(12)
11.0
这两个示例有共通之处:调用Averager()或make_averager()得到一个可调用对象avg,它会更新历史值,然后计算当前均值。在示例1中,avg是Averager的实例;在示例2中,是内部函数averager。都只需调用avg(n),把n放入系列值中,然后重新计算均值。
series是make_averager函数的局部变量,因为那个函数的定义体中初始化了series:series = []。可是,调用avg(10)时,make_averager函数已经返回了,而它的本地作用域也一去不复返了。
在averager函数中,series是自由变量(free variable)。这是一个技术术语,指未在本地作用域中绑定的变量,如下图所示:
审查返回的averager对象,发现Python在__code__属性(表示编译后的函数定义体)中保存局部变量和自由变量的名称,如下示例3:
#示例3:审查make_averager(见示例2)创建的函数
>>> avg.__code__.co_varnames
('new_value', 'total')
>>> avg.__code__.co_freevars
('series',)
series的绑定在返回的avg函数的__closure__属性中。avg.__closure__中的各个元素对应于avg.code.co_freevars中的一个名称。这些元素是cell对象,有个cell_contents属性,保存着真正的值。这些属性的值如下示例4:
#接续示例3
>>> avg.__code__.co_freevars
('series',)
>>> avg.__closure__
(<cell at 0x107a44f78: list object at 0x107a91a48>,)
>>> avg.__closure__[0].cell_contents
[10, 11, 12]
综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。
注意,只有嵌套在其他函数中的函数才可能需要处理不在全局作用域中的外部变量。
nonlocal声明
前面实现make_averager函数的方法效率不高,更好的实现方式是,只存储目前的总值和元素个数,然后使用这两个数计算均值。
def make_averager():
count = 0
total = 0
def averager(new_value):
count +=1
total +=new_value
return total/count
return averager
>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
...
UnboundLocalError: local variable 'count' referenced before assignment
>>>
当count是数字或任何不可变类型时,count += 1语句的作用其实与count = count + 1一样。因此,我们在averager的定义体中为count赋值了,这会把count变成局部变量。total变量也受这个问题影响。
对数字、字符串、元组等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如count = count + 1,其实会隐式创建局部变量count。这样,count就不是自由变量了,因此不会保存在闭包中。
为了解决这个问题,Python 3引入了nonlocal声明。它的作用是把变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。如果为nonlocal声明的变量赋予新值,闭包中保存的绑定会更新。
计算移动平均值,不保存所有历史(使用nonlocal修正)
def make_averager():
count = 0
total = 0
def averager(new_value):
nonlocal count, total
count += 1
total += new_value
return total / count
return averager
我们了解了Python闭包,下面可以使用嵌套函数正式实现装饰器了。