首页 文章资讯内容详情

理解Python闭包概念

2026-06-01 4 花语

本文内容纲要:

-1.概念介绍 -2.闭包初探 -3.闭包陷阱 -4.闭包的应用 -5.闭包的实现

闭包并不只是一个python中的概念,在函数式编程语言中应用较为广泛。理解python中的闭包一方面是能够正确的使用闭包,另一方面可以好好体会和思考闭包的设计思想。

1.概念介绍

首先看一下维基上对闭包的解释:

在计算机科学中,闭包(英语:Closure),又称词法闭包(LexicalClosure)或函数闭包(functionclosures),是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。

简单来说就是一个函数定义中引用了函数外定义的变量,并且该函数可以在其定义环境外被执行。这样的一个函数我们称之为闭包。实际上闭包可以看做一种更加广义的函数概念。因为其已经不再是传统意义上定义的函数。

根据我们对编程语言中函数的理解,大概印象中的函数是这样的:

程序被加载到内存执行时,函数定义的代码被存放在代码段中。函数被调用时,会在栈上创建其执行环境,也就是初始化其中定义的变量和外部传入的形参以便函数进行下一步的执行操作。当函数执行完成并返回函数结果后,函数栈帧便会被销毁掉。函数中的临时变量以及存储的中间计算结果都不会保留。下次调用时唯一发生变化的就是函数传入的形参可能会不一样。函数栈帧会重新初始化函数的执行环境。

C++中有static关键字,函数中的static关键字定义的变量独立于函数之外,而且会保留函数中值的变化。函数中使用的全局变量也有类似的性质。

但是闭包中引用的函数定义之外的变量是否可以这么理解呢?但是如果函数中引用的变量既不是全局的,也不是静态的(python中没有这个概念)。应该怎么正确的理解呢?

建议先参考一下我的另一篇博文(PythonUnboundLocalError和NameError错误根源解析),了解一下变量可见性和绑定相关的概念非常有必要。

2.闭包初探

为了说明闭包中引用的变量的性质,可以看一下下面的这个例子:

1defouter_func(): 2loc_list=[] 3definner_func(name): 4loc_list.append(len(loc_list)+1) 5print%sloc_list=%s%(name,loc_list) 6returninner_func 7 8clo_func_0=outer_func() 9clo_func_0(clo_func_0) 10clo_func_0(clo_func_0) 11clo_func_0(clo_func_0) 12clo_func_1=outer_func() 13clo_func_1(clo_func_1) 14clo_func_0(clo_func_0) 15clo_func_1(clo_func_1)

程序的运行结果:

clo_func_0loc_list=[1]

clo_func_0loc_list=[1,2]

clo_func_0loc_list=[1,2,3]

clo_func_1loc_list=[1]

clo_func_0loc_list=[1,2,3,4]

clo_func_1loc_list=[1,2]

从上面这个简单的例子应该对闭包有一个直观的理解了。运行的结果也说明了闭包函数中引用的父函数中localvariable既不具有C++中的全局变量的性质也没有static变量的行为。

在python中我们称上面的这个loc_list为闭包函数inner_func的一个自由变量(freevariable)。

Ifanameisboundinablock,itisalocalvariableofthatblock.Ifanameisboundatthemodulelevel,itisaglobalvariable.(Thevariablesofthemodulecodeblockarelocalandglobal.)Ifavariableisusedinacodeblockbutnotdefinedthere,itisafreevariable.

在这个例子中我们至少可以对闭包中引用的自由变量有如下的认识:

闭包中的引用的自由变量只和具体的闭包有关联,闭包的每个实例引用的自由变量互不干扰。 一个闭包实例对其自由变量的修改会被传递到下一次该闭包实例的调用。

由于这个概念理解起来并不是那么的直观,因此使用的时候很容易掉进陷阱。

3.闭包陷阱

下面先来看一个例子:

1defmy_func(*args): 2fs=[] 3foriinxrange(3): 4deffunc(): 5returni*i 6fs.append(func) 7returnfs 8 9fs1,fs2,fs3=my_func() 10printfs1() 11printfs2() 12printfs3()

上面这段代码可谓是典型的错误使用闭包的例子。程序的结果并不是我们想象的结果0,1,4。实际结果全部是4。

这个例子中,my_func返回的并不是一个闭包函数,而是一个包含三个闭包函数的一个list。这个例子中比较特殊的地方就是返回的所有闭包函数均引用父函数中定义的同一个自由变量。

但这里的问题是为什么for循环中的变量变化会影响到所有的闭包函数?尤其是我们上面刚刚介绍的例子中明明说明了同一闭包的不同实例中引用的自由变量互相没有影响的。而且这个观点也绝对的正确。

那么问题到底出在哪里?应该怎样正确的分析这个错误的根源。

其实问题的关键就在于在返回闭包列表fs之前for循环的变量的值已经发生改变了,而且这个改变会影响到所有引用它的内部定义的函数。因为在函数my_func返回前其内部定义的函数并不是闭包函数,只是一个内部定义的函数。

当然这个内部函数引用的父函数中定义的变量也不是自由变量,而只是当前block中的一个localvariable。

1defmy_func(*args): 2fs=[] 3j=0 4foriinxrange(3): 5deffunc(): 6returnj*j 7fs.append(func) 8j=2 9returnfs

上面的这段代码逻辑上与之前的例子是等价的。这里或许更好理解一点,因为在内部定义的函数func实际执行前,对局部变量j的任何改变均会影响到函数func的运行结果。

函数my_func一旦返回,那么内部定义的函数func便是一个闭包,其中引用的变量j成为一个只和具体闭包相关的自由变量。后面会分析,这个自由变量存放在Cell对象中。

使用lambda表达式重写这个例子:

1defmy_func(*args): 2fs=[] 3foriinxrange(3): 4func=lambda:i*i 5fs.append(func) 6returnfs

经过上面的分析,我们得出下面一个重要的经验:返回闭包中不要引用任何循环变量,或者后续会发生变化的变量。

这条规则本质上是在返回闭包前,闭包中引用的父函数中定义变量的值可能会发生不是我们期望的变化。

正确的写法

1defmy_func(*args): 2fs=[] 3foriinxrange(3): 4deffunc(_i=i): 5return_i*_i 6fs.append(func) 7returnfs

或者:

1defmy_func(*args): 2fs=[] 3foriinxrange(3): 4func=lambda_i=i:_i*_i 5fs.append(func) 6returnfs

正确的做法便是将父函数的localvariable赋值给函数的形参。函数定义时,对形参的不同赋值会保留在当前函数定义中,不会对其他函数有影响。

另外注意一点,如果返回的函数中没有引用父函数中定义的localvariable,那么返回的函数不是闭包函数。

4.闭包的应用

自由变元可以记录闭包函数被调用的信息,以及闭包函数的一些计算结果中间值。而且被自由变量记录的值,在下次调用闭包函数时依旧有效。

根据闭包函数中引用的自由变量的一些特性,闭包的应用场景还是比较广泛的。后面会有文章介绍其应用场景之一——单例模式,限于篇幅,此处以装饰器为例介绍一下闭包的应用。

如果我们想对一个函数或者类进行修改重定义,最简单的方法就是直接修改其定义。但是这种做法的缺点也是显而易见的:

可能看不到函数或者类的定义 会破坏原来的定义,导致原来对类的引用不兼容 如果多人想在原来的基础上定制自己函数,很容易冲突

使用闭包可以相对简单的解决上面的问题,下面看一个例子:

1deffunc_dec(func): 2defwrapper(*args): 3iflen(args)==2: 4func(*args) 5else: 6printError!Arguments=%s%list(args) 7returnwrapper 8 9@func_dec 10defadd_sum(*args): 11printsum(args) 12 13#add_sum=func_dec(add_sum) 14args=range(1,3) 15add_sum(*args)

对于上面的这个例子,并没有破坏add_sum函数的定义,只不过是对其进行了一层简单的封装。如果看不到函数的定义,也可以对函数对象进行封装,达到相同的效果(即上面注释掉的13行),而且装饰器是可以叠加使用的。

4.1潜在的问题

但闭包的缺点也是很明显的,那就是经过装饰器装饰的函数或者类不再是原来的函数或者类了。这也是使用装饰器改变函数或者类的行为与直接修改定义最根本的差别。

实际应用的时候一定要注意这一点,下面看一个使用装饰器导致的一个很隐蔽的问题。

1defcounter(cls): 2obj_list=[] 3defwrapper(*args,**kwargs): 4new_obj=cls(*args,**kwargs) 5obj_list.append(new_obj) 6print"class:%sobjectnumberis%d"%(cls.__name__,len(obj_list)) 7returnnew_obj 8returnwrapper 9 10@counter 11classmy_cls(object): 12STATIC_MEM=Thisisastaticmemberofmy_cls 13def__init__(self,*args,**kwargs): 14printself,args,kwargs 15printmy_cls.STATIC_MEM

这个例子中我们尝试使用装饰器来统计一个类创建的对象数量。当我们创建my_cls的对象时,会发现somethingiswrong!

Traceback(mostrecentcalllast): File"G:\Cnblogs\AlphaPanda\Main.py",line360,in<module> my_cls(1,2,key=shijun) File"G:\Cnblogs\AlphaPanda\Main.py",line347,inwrapper new_obj=cls(*args,**kwargs) File"G:\Cnblogs\AlphaPanda\Main.py",line358,in__init__ printmy_cls.STATIC_MEM AttributeError:functionobjecthasnoattributeSTATIC_MEM

如果对装饰器不是特别的了解,可能会对这个错误感到诧异。经过装饰器修饰后,我们定义的类my_cls已经成为一个函数。

my_cls.__name__==wrapperandtype(my_cls)istypes.FunctionType

my_cls被装饰器counter修饰,等价于my_cls=counter(my_cls)

显然在上面的例子中,my_cls.STATIC_MEM是错误的,正确的用法是self.STATIC_MEM。

对象中找不到属性的话,会到类空间中寻找,因此被装饰器修饰的类的静态属性是可以通过其对象进行访问的。虽然my_cls已经不是类,但是其调用返回的值却是被装饰之前的类的对象。

该问题同样适用于staticmethod。那么有没有方法得到原来的类呢?当然可以,my_cls().__class__便是被装饰之前的类的定义。

那有没有什么方法能让我们还能通过my_cls来访问类的静态属性,答案是肯定的。

1defcounter(cls): 2obj_list=[] 3@functools.wraps(cls) 4defwrapper(*args,**kwargs): 5...... 6returnwrapper

改写装饰器counter的定义,主要是对wrapper使用functools进行了一次包裹更新,使经过装饰的my_cls看起来更像装饰之前的类或者函数。该过程的主要原理就是将被装饰类或者函数的部分属性直接赋值到装饰之后的对象。如WRAPPER_ASSIGNMENTS(__name__,__module__and__doc__,)和WRAPPER_UPDATES(__dict__)等。但是该过程不会改变wrapper是函数这样一个事实。

my_cls.__name__==my_clsandtype(my_cls)istypes.FunctionType

5.闭包的实现

本着会用加理解的原则,可以从应用层的角度来稍微深入的理解一下闭包的实现。毕竟要先会用python么,如果一切都从源码中学习,那成本的确有点高。

1defouter_func(): 2loc_var="localvariable" 3definner_func(): 4returnloc_var 5returninner_func 6 7importdis 8dis.dis(outer_func) 9clo_func=outer_func() 10printclo_func() 11dis.dis(clo_func)

为了更加清楚理解上述过程,我们先尝试给出outer_func.func_code中的部分属性:

outer_func.func_code.co_consts:(None,localvariable,<codeobjectinner_funcat025F7770,file"G:\Cnblogs\AlphaPanda\Main.py",line207>) outer_func.func_code.co_cellvars:(loc_var,) outer_func.func_code.co_varnames:(inner_func,)

尝试反汇编上面这个简单清晰的闭包例子,得到下面的结果:

20LOAD_CONST1(localvariable)#将outer_func.func_code.co_consts[1]放到栈顶 3STORE_DEREF0(loc_var)#将栈顶元素存放到cell对象的slot0 36LOAD_CLOSURE0(loc_var)#将outer_func.func_code.co_cellvars[0]对象的索引放到栈顶 9BUILD_TUPLE1#将栈顶1个元素取出,创建元组并将元组压入栈中 12LOAD_CONST2(<codeobjectinner_funcat02597770,file"G:\Cnblogs\AlphaPanda\Main.py",line207>)#将outer_func.func_code.co_consts[2]放到栈顶 15MAKE_CLOSURE0#创建闭包,此时栈顶是闭包函数代码段的入口,栈顶下面则是函数的freevariables,也就是本例中的localvariable,将闭包压入栈顶 18STORE_FAST0(inner_func)#将栈顶存放入outer_func.func_code.co_varnames[0] 521LOAD_FAST0(inner_func)#将outer_func.func_code.co_varnames[0]的引用放入栈顶 24RETURN_VALUE#ReturnswithTOStothecallerofthefunction. localvariable 40LOAD_DEREF0(loc_var)#将cell对象中的slot0对象的引用压入栈顶 3RETURN_VALUE#ReturnswithTOStothecallerofthefunction

这个结果中,我们反汇编了外层函数及其返回的闭包函数(为了便于查看,修改了部分行号)。从对上面两个函数的反汇编的注释可以大致了解闭包实现的步骤。

python闭包中引用的自由变量实际存放在一个Cell对象中,当自由变元被闭包引用时,便将Cell中存放的自由变量的引用放入栈顶。

本例中Cell对象及其存放的自由变量分别为:

clo_func.func_closure[0]#CellObject clo_func.func_closure[0].cell_contents==localvariable#FreeVariable

闭包实现的一个关键的地方是CellObject,下面是官方给出的解释:

“Cell”objectsareusedtoimplementvariablesreferencedbymultiplescopes.Foreachsuchvariable,acellobjectiscreatedtostorethevalue;thelocalvariablesofeachstackframethatreferencesthevaluecontainsareferencetothecellsfromouterscopeswhichalsousethatvariable.Whenthevalueisaccessed,thevaluecontainedinthecellisusedinsteadofthecellobjectitself.Thisde-referencingofthecellobjectrequiressupportfromthegeneratedbyte-code;thesearenotautomaticallyde-referencedwhenaccessed.Cellobjectsarenotlikelytobeusefulelsewhere.

好了,限于篇幅就先介绍到这里。重要的是理解的基础上灵活的应用解决实际的问题并避免陷阱,希望本文能让你对闭包有一个不一样的认识。

本文内容总结:1.概念介绍,2.闭包初探,3.闭包陷阱,4.闭包的应用,5.闭包的实现,

原文链接:https://www.cnblogs.com/yssjun/p/9887239.html