当前位置: 首页 > 科技观察

关于Python闭包的一切

时间:2023-03-15 19:39:22 科技观察

本文转载自微信公众号“东方儿”,作者东方儿。转载本文请联系东方儿公众号。任何将函数视为一等对象的语言,它的设计者都不得不面对一个问题:作为一等对象的函数是在某个范围内定义的,但可能在其他范围内被调用,如何处理自由变量?自由变量是未绑定在局部范围内的变量。为了解决这个问题,Python之父GuidoVanRossum设计了闭包,闭包犹如神来之笔,将代码美学发挥得淋漓尽致。在讨论闭包之前,有必要了解Python中的变量作用域。变量作用域先看一个全局变量和自由变量的例子:>>>b=6>>>deff1(a):...print(a)...print(b)...>>>f1(3)36函数体外面的b是全局变量,函数体里面的b是自由变量。因为自由变量b绑定了全局变量,所以在函数f1()中可以正确打印。如果稍作改动,那么函数体中的b就会由自由变量变为局部变量:>>>b=6deff1(a):...print(a)...print(b)...b=9...>>>f1(3)3Traceback(mostrecentcallast):File"",line1,inFile"",line3,inf1UnboundLocalError:localvariable'b'referencedbeforeassignment落后于函数f1()Addb=9报错:赋值前引用了局部变量b。这不是缺陷,而是Python的设计:Python不需要声明变量,而是假设在函数定义体中赋值的变量是局部变量。如果你想让解释器把b当作一个全局变量,你需要使用全局声明:>>>b=6>>>deff1(a):...globalb...print(a)...print(二)...b=9...>>>f1(3)36Closure回到文章开头的自由变量问题,如果有一个函数叫做avg,它的作用是计算一系列的平均值值,并用类实现它:classAverager():def__init__(self):self.series=[]def__call__(self,new_value):self.series.append(new_value)total=sum(self.series)returntotle/len(self.series)avg=Averager()av??g(10)#10.0avg(11)#10.5avg(12)#11.0类实现不存在自由变量的问题,因为self.series是类属性。但是当函数实现时,函数嵌套时问题就出现了:defmake_averager():series=[]defaverager(new_value):#series是一个自由变量series.append(new_value)total=sum(series)returntottle/len(series)returnaverageravg=make_averager()avg(10)#10.0avg(11)#10.5avg(12)#11.0函数make_averager()定义了局部范围内的series变量,其内部函数averager的自由变量series()绑定设置这个值。但是当调用avg(10)时,make_averager()函数已经返回,并且它的局部作用域消失了。如果没有闭包,自由变量系列肯定会报找不到定义的错误。那么闭包是如何做到的呢?闭包是一个函数,它保留定义时存在的自由变量的绑定,这样当函数被调用时,虽然定义范围不再可用,但仍然可以使用那些绑定。如下图所示:闭包会保留自由变量series的绑定,并在调用avg(10)时继续使用这个绑定,即使make_averager()函数的局部作用域已经消失。nonlocal稍微优化了上面例子的需求,只存储当前总值和元素个数:defmake_averager():count=0total=0defaverager(new_value):count+=1total+=new_valuereturntotal/countreturnaverager运行后会报错:局部变量计数在赋值前被引用。因为count+=1等价于count=count+1,所以有赋值,count变成局部变量。总数也是如此。通过global关键字将count和total声明为全局变量显然是不合适的,它们的作用域最多只扩展到make_averager()函数。为了解决这个问题,Python3引入了nonlocal关键字语句:defmake_averager():count=0total=0defaverager(new_value):nonlocalcount,totalcount+=1total+=new_valuereturntotal/countreturnaveragernonlocal用于标记变量为自由变量,即使在functionas变量被赋值,但它仍然是一个自由变量。请注意,对于列表和字典等可变类型,添加元素不是赋值,并且不会隐式创建局部变量。对于数字、字符串、元组和None等不可变类型,赋值会隐式创建局部变量。例子:defmake_averager():#variabletypecount={}defaverager(new_value):print(count)#successcount[new_value]=new_valuereturncountreturnaveragervariableobject添加元素不是赋值,不会隐式创建局部变量。defmake_averager():#immutabletypecount=1defaverager(new_value):print(count)#reporterrorcount=new_valuereturncountreturnaveragercount是不可变类型,赋值会隐式创建局部变量,错误:局部变量count在赋值前被引用。defmake_averager():#Nonecount=Nonedefaverager(new_value):print(count)#errorcount=new_valuereturncountreturnaveragercount为None,赋值会隐式创建局部变量,错误:局部变量count在赋值前被引用。小结本文首先介绍了全局变量、自由变量、局部变量的概念,这是理解闭包的前提。闭包用于解决函数嵌套时如何处理自由变量的问题。即使本地作用域消失了,它也会保留自由变量的绑定。对于不可变类型和None,赋值会隐式创建局部变量并将自由变量转换为局部变量,这可能会导致程序错误:局部变量在赋值前被引用。除了使用全局声明作为全局变量外,还可以使用非局部声明将局部变量强制变为自由变量,从而实现闭包。