一、作用域规则
学习闭包之前,先来了解python的作用域规则
b = 6 def f1(a): print(a) print(b) f1(3) # 输出 # 3 # 6
这个大家应该都懂,但是下面这个估计有人不明白了。
首先输出了3,说明print(a)语句执行了。但是第二个语句print(b)执行不了。为什么执行不了呢,不是有个全局变量b吗,而且是在print(b)之后为局部变量b赋值的。
实际上,python编译函数的定义体时,它判断b是一个局部变量,因为在函数中给它赋值了。生成的字节码证实了这种判断,python会尝试从本地环境获取b,后面调用f1(3)时,f1的定义体会获取并打印局部变量a的值,但是尝试获取局部变量b的值时,发现b没有绑定值。
这不是缺陷,而是设计选择:python不要求声明变量,但是假定在函数定义体中赋值得变量是局部变量。如果在函数中赋值时想让解释器把b当做全局变量,要使用global声明:
二、闭包
首先来看一个计算移动平均值的高阶函数
def make_average(): series = [] def averager(val): series.append(val) total = sum(series) return total / len(series) return averager avg = make_average() print(avg(10)) #10.0 print(avg(11)) #10.5 print(avg(12)) #11.0
series是make_average函数的局部变量,因为那个函数的定义体中已经初始化了series=[]
可是,调用avg(10)时,make_average函数已经返回了,而它的本地作用域也一去不复返了。为什么调用avg(11)时,结果是11.0呢?它的10是怎么保留在series的呢,调用avg(11)时不会初始化series=[] 吗?
原来,在averager函数中,series是自由变量,自由变量指未在本地作用域绑定的变量。
在审核返回的averager对象时,发现python在__code__属性(表示编译后的函数定义体)中保存局部变量和自由变量的名称。
series的绑定在返回的avg函数的__closure__属性中。avg._closure__中的各个元素对应于avg.code.co_freevars中的名称。这些元素是cell对象,有个cell_contents属性,保存着真正的值。
所以在上面的例子中,averager对象仍保存着自由变量series的值。
综上,闭包是一种函数,它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍然能使用那些绑定。
注意,只有嵌套在其它函数中的函数才可能需要处理不在全局作用域的外部变量。
三、nonlocal声明
前面实现计算移动平均值的方法中,将值存储在历史数组中,然后每次调用函数使用sum求和,这样处理的效率不高。更好的实现方式是,只储存目前的总值和元素个数,然后计算均值。
def make_averager(): count = 0 total = 0 def averager(val): count += 1 total += val return total / count return averager
然而运行会报错
这是为什么呢?这是因为当count是数字或者任何不可变类型时,count += 1语句的作用其实与count = count + 1 一样。因此,我们在averager的定义体中为count赋值了,这会把count变成局部变量。total变量也受这个问题影响。
前面的示例使用series = [],没有遇到这个问题,因为我们没有为series赋值,我们只是调用series.append,并把它传给sum 和 len。也就是说,我们利用了列表是可变这一事实。
但对于数字,元组,字符串等不可变类型来说,只能读取,不能更新。如果尝试重新绑定,例如count = count + 1,其实会隐式创建局部变量count。这样,count就不是自由变量了,因此不会保存在闭包中。
为了解决这个问题,python3引入了nonlocal声明。它的作用是变量标记为自由变量,即使在函数中为变量赋予新值了,也会变成自由变量。如果为nonlocal声明的变量赋予新值,闭包中保存的绑定会更新。
问题解决,如下所示:
参考自:《流畅的Python》
————————————————
版权声明:本文为CSDN博主「发狂的桔子」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_43352942/article/details/102963706