理解Python闭包概念
童话故事集闭包并不只是⼀个python中的概念,在函数式编程语⾔中应⽤较为⼴泛。理解python中的闭包⼀⽅⾯是能够正确的使⽤闭包,另⼀⽅⾯可以好好体会和思考闭包的设计思想。
1.概念介绍
⾸先看⼀下维基上对闭包的解释:
在计算机科学中,闭包(英语:Closure),⼜称词法闭包(Lexical Closure)或函数闭包(function closures),是引⽤了⾃由变量的函数。这个被引⽤的⾃由变量将和这个函数⼀同存在,即使已经离开了创造它的环境也不例外。所以,有另⼀种说法认为闭包是由函数和与其相关的引⽤环境组合⽽成的实体。
闭包在运⾏时可以有多个实例,不同的引⽤环境和相同的函数组合可以产⽣不同的实例。
简单来说就是⼀个函数定义中引⽤了函数外定义的变量,并且该函数可以在其定义环境外被执⾏。这样的⼀个函数我们称之为闭包。实际上闭包可以看做⼀种更加⼴义的函数概念。因为其已经不再是传统意义上定义的函数。
根据我们对编程语⾔中函数的理解,⼤概印象中的函数是这样的:
程序被加载到内存执⾏时,函数定义的代码被存放在代码段中。函数被调⽤时,会在栈上创建其执⾏环境,也就是初始化其中定义的变量和外部传⼊的形参以便函数进⾏下⼀步的执⾏操作。当函数执⾏完成并返回函数结果后,函数栈帧便会被销毁掉。函数中的临时变量以及存储的中间计算结果都不会保留。下次调⽤时唯⼀发⽣变化的就是函数传⼊的形参可能会不⼀样。函数栈帧会重新初始化函数的执⾏环境。
C++中有static关键字,函数中的static关键字定义的变量独⽴于函数之外,⽽且会保留函数中值的变化。函数中使⽤的全局变量也有类似的性质。
但是闭包中引⽤的函数定义之外的变量是否可以这么理解呢?但是如果函数中引⽤的变量既不是全局的,也不是静态的(python中没有这个概念)。应该怎么正确的理解呢?
建议先参考⼀下我的另⼀篇博⽂(),了解⼀下变量可见性和绑定相关的概念⾮常有必要。
2.闭包初探
为了说明闭包中引⽤的变量的性质,可以看⼀下下⾯的这个例⼦:
1def outer_func():
2 loc_list = []
3def inner_func(name):
百家讲坛苏轼4 loc_list.append(len(loc_list) + 1)
5print'%s loc_list = %s' %(name, loc_list)
6return inner_func
7
8 clo_func_0 = outer_func()
9 clo_func_0('clo_func_0')
酥脆小麻花配方
10 clo_func_0('clo_func_0')
11 clo_func_0('clo_func_0')
12 clo_func_1 = outer_func()
13 clo_func_1('clo_func_1')
14 clo_func_0('clo_func_0')
四川风景15 clo_func_1('clo_func_1')
程序的运⾏结果:
clo_func_0 loc_list = [1]
clo_func_0 loc_list = [1, 2]
clo_func_0 loc_list = [1, 2, 3]
clo_func_1 loc_list = [1]
clo_func_0 loc_list = [1, 2, 3, 4]
clo_func_1 loc_list = [1, 2]
从上⾯这个简单的例⼦应该对闭包有⼀个直观的理解了。运⾏的结果也说明了闭包函数中引⽤的⽗函数中local variable既不具有C++中的全局变量的性质也没有static变量的⾏为。
在python中我们称上⾯的这个loc_list为闭包函数inner_func的⼀个⾃由变量(free variable)。
If a name is bound in a block, it is a local variable of that block. If a name is bound at the module level, it is a global variable. (The variables of the module code block are local and global.) If a variable is ud in a code block but not defined there, it is a free variable.
在这个例⼦中我们⾄少可以对闭包中引⽤的⾃由变量有如下的认识:
闭包中的引⽤的⾃由变量只和具体的闭包有关联,闭包的每个实例引⽤的⾃由变量互不⼲扰。
⼀个闭包实例对其⾃由变量的修改会被传递到下⼀次该闭包实例的调⽤。
由于这个概念理解起来并不是那么的直观,因此使⽤的时候很容易掉进陷阱。
3.闭包陷阱
下⾯先来看⼀个例⼦:
1def my_func(*args):
2 fs = []
3for i in xrange(3):
4def func():
5return i * i
6 fs.append(func)
7return fs
8
9 fs1, fs2, fs3 = my_func()
10print fs1()
11print fs2()
12print fs3()
上⾯这段代码可谓是典型的错误使⽤闭包的例⼦。程序的结果并不是我们想象的结果0,1,4。实际结果全部是4。
这个例⼦中,my_func返回的并不是⼀个闭包函数,⽽是⼀个包含三个闭包函数的⼀个list。这个例⼦中⽐较特殊的地⽅就是返回的所有闭包函数均引⽤⽗函数中定义的同⼀个⾃由变量。
但这⾥的问题是为什么for循环中的变量变化会影响到所有的闭包函数?尤其是我们上⾯刚刚介绍的例⼦中明明说明了同⼀闭包的不同实例中引⽤的⾃由变量互相没有影响的。⽽且这个观点也绝对的正确。
那么问题到底出在哪⾥?应该怎样正确的分析这个错误的根源。
其实问题的关键就在于在返回闭包列表fs之前for循环的变量的值已经发⽣改变了,⽽且这个改变会影响到所有引⽤它的内部定义的函数。因为在函数my_func返回前其内部定义的函数并不是闭包函数,只是⼀个内部定义的函数。
当然这个内部函数引⽤的⽗函数中定义的变量也不是⾃由变量,⽽只是当前block中的⼀个local variable。
1 def my_func(*args):
2 fs = []
3 j = 0
4for i in xrange(3):
5def func():
6return j * j
7 fs.append(func)
8 j = 2
9return fs
上⾯的这段代码逻辑上与之前的例⼦是等价的。这⾥或许更好理解⼀点,因为在内部定义的函数func实际执⾏前,对局部变量j的任何改变均会影响到函数func的运⾏结果。
函数my_func⼀旦返回,那么内部定义的函数func便是⼀个闭包,其中引⽤的变量j成为⼀个只和具体闭包相关的⾃由变量。后⾯会分析,这个⾃由变量存放在Cell对象中。
使⽤lambda表达式重写这个例⼦:
1def my_func(*args):
2 fs = []
3for i in xrange(3):
4 func = lambda : i * i
5 fs.append(func)
6return fs
经过上⾯的分析,我们得出下⾯⼀个重要的经验:返回闭包中不要引⽤任何循环变量,或者后续会发⽣变化的变量。
这条规则本质上是在返回闭包前,闭包中引⽤的⽗函数中定义变量的值可能会发⽣不是我们期望的变化。
正确的写法:
1def my_func(*args):
2 fs = []
3for i in xrange(3):
4def func(_i = i):
5return _i * _i
如何做海外代购生意6 fs.append(func)
7return fs
或者:
1def my_func(*args):
2 fs = []
3for i in xrange(3):
4 func = lambda _i = i : _i * _i
5 fs.append(func)
6return fs
正确的做法便是将⽗函数的local variable赋值给函数的形参。函数定义时,对形参的不同赋值会保留在当前函数定义中,不会对其他函数有影响。
另外注意⼀点,如果返回的函数中没有引⽤⽗函数中定义的local variable,那么返回的函数不是闭包函数。
4.闭包的应⽤
⾃由变元可以记录闭包函数被调⽤的信息,以及闭包函数的⼀些计算结果中间值。⽽且被⾃由变量记录的值,在下次调⽤闭包函数时依旧有效。
根据闭包函数中引⽤的⾃由变量的⼀些特性,闭包的应⽤场景还是⽐较⼴泛的。后⾯会有⽂章介绍其应⽤场景之⼀——单例模式,限于篇幅,此处以装饰器为例介绍⼀下闭包的应⽤。
如果我们想对⼀个函数或者类进⾏修改重定义,最简单的⽅法就是直接修改其定义。但是这种做法的缺点也是显⽽易见的:
可能看不到函数或者类的定义
会破坏原来的定义,导致原来对类的引⽤不兼容
如果多⼈想在原来的基础上定制⾃⼰函数,很容易冲突
使⽤闭包可以相对简单的解决上⾯的问题,下⾯看⼀个例⼦:
1def func_dec(func):
2def wrapper(*args):
3if len(args) == 2:
4 func(*args)
5el:
6print'Error! Arguments = %s'%list(args)
7return wrapper
8
9 @func_dec
10def add_sum(*args):
下载经典老歌11print sum(args)
12
13# add_sum = func_dec(add_sum)
14 args = range(1,3)
15 add_sum(*args)
对于上⾯的这个例⼦,并没有破坏add_sum函数的定义,只不过是对其进⾏了⼀层简单的封装。如果看不到函数的定义,也可以对函数对象进⾏封装,达到相同的效果(即上⾯注释掉的13⾏),⽽且装饰器是可以叠加使⽤的。
4.1 潜在的问题
但闭包的缺点也是很明显的,那就是经过装饰器装饰的函数或者类不再是原来的函数或者类了。这也
是使⽤装饰器改变函数或者类的⾏为与直接修改定义最根本的差别。
实际应⽤的时候⼀定要注意这⼀点,下⾯看⼀个使⽤装饰器导致的⼀个很隐蔽的问题。
1def counter(cls):
2 obj_list = []
3def wrapper(*args, **kwargs):
4 new_obj = cls(*args, **kwargs)
5 obj_list.append(new_obj)
6print"class:%s'object number is %d" % (cls.__name__, len(obj_list))
7return new_obj
8return wrapper
9
10 @counter
11class my_cls(object):
12 STATIC_MEM = 'This is a static member of my_cls'
13def__init__(lf, *args, **kwargs):
14print lf, args, kwargs
15print my_cls.STATIC_MEM
这个例⼦中我们尝试使⽤装饰器来统计⼀个类创建的对象数量。当我们创建my_cls的对象时,会发现something is wrong!
Traceback (most recent call last):
File "G:\Cnblogs\Alpha Panda\Main.py", line 360, in <module>
my_cls(1,2, key = 'shijun')
File "G:\Cnblogs\Alpha Panda\Main.py", line 347, in wrapper
new_obj = cls(*args, **kwargs)
File "G:\Cnblogs\Alpha Panda\Main.py", line 358, in__init__
print my_cls.STATIC_MEM
AttributeError: 'function' object has no attribute 'STATIC_MEM'
如果对装饰器不是特别的了解,可能会对这个错误感到诧异。经过装饰器修饰后,我们定义的类my_cls已经成为⼀个函数。
my_cls.__name__ == 'wrapper'and type(my_cls) is types.FunctionType
my_cls被装饰器counter修饰,等价于 my_cls = counter(my_cls)。
显然在上⾯的例⼦中,my_cls.STATIC_MEM是错误的,正确的⽤法是lf.STATIC_MEM。
对象中找不到属性的话,会到类空间中寻找,因此被装饰器修饰的类的静态属性是可以通过其对象进⾏访问的。虽然my_cls已经不是类,但是其调⽤返回的值却是被装饰之前的类的对象。
该问题同样适⽤于staticmethod。那么有没有⽅法得到原来的类呢?当然可以,my_cls().__class__便
是被装饰之前的类的定义。
那有没有什么⽅法能让我们还能通过my_cls来访问类的静态属性,答案是肯定的。
1def counter(cls):
2 obj_list = []
3 @functools.wraps(cls)
4def wrapper(*args, **kwargs):
5 ... ...
6return wrapper
改写装饰器counter的定义,主要是对wrapper使⽤functools进⾏了⼀次包裹更新,使经过装饰的my_cls看起来更像装饰之前的类或者函数。该过程的主要原理就是将被装饰类或者函数的部分属性直接赋值到装饰之后的对象。如WRAPPER_ASSIGNMENTS(__name__, __module__ and __doc__, )和
WRAPPER_UPDATES(__dict__)等。但是该过程不会改变wrapper是函数这样⼀个事实。
my_cls.__name__ == 'my_cls'and type(my_cls) is types.FunctionType
5.闭包的实现
本着会⽤加理解的原则,可以从应⽤层的⾓度来稍微深⼊的理解⼀下闭包的实现。毕竟要先会⽤python么,如果⼀切都从源码中学习,那成本的确有点⾼。
1def outer_func():
2 loc_var = "local variable"
3def inner_func():
4return loc_var
5return inner_func
6
7import dis
c902
8 dis.dis(outer_func)
9 clo_func = outer_func()
10print clo_func()
11 dis.dis(clo_func)
为了更加清楚理解上述过程,我们先尝试给出outer_func.func_code中的部分属性:
outer_func._consts: (None, 'local variable', <code object inner_func at 025F7770, file "G:\Cnblogs\Alpha Panda\Main.py", line 207>)
outer_func._cellvars:('loc_var',)
outer_func._varnames:('inner_func',)
尝试反汇编上⾯这个简单清晰的闭包例⼦,得到下⾯的结果:
2 0 LOAD_CONST 1 ('local variable') # 将outer_func._consts[1]放到栈顶
3 STORE_DEREF 0 (loc_var) # 将栈顶元素存放到cell对象的slot 0
戍轮台3 6 LOAD_CLOSURE 0 (loc_var) # 将outer_func._cellvars[0]对象的索引放到栈顶
9 BUILD_TUPLE 1 # 将栈顶1个元素取出,创建元组并将元组压⼊栈中
12 LOAD_CONST 2 (<code object inner_func at 02597770, file "G:\Cnblogs\Alpha Panda\Main.py", line 207>) # 将outer_func._consts[2]放到栈顶
15 MAKE_CLOSURE 0 # 创建闭包,此时栈顶是闭包函数代码段的⼊⼝,栈顶下⾯则是函数的free variables,也就是本例中的'local variable ',将闭包压⼊栈顶 18 STORE_FAST 0 (inner_func) # 将栈顶存放⼊outer_func._varnames[0]
5 21 LOAD_FAST 0 (inner_func) # 将outer_func._varnames[0]的引⽤放⼊栈顶
24 RETURN_VALUE # Returns with TOS to the caller of the function.
local variable
4 0 LOAD_DEREF 0 (loc_var) # 将cell对象中的slot 0对象的引⽤压⼊栈顶
3 RETURN_VALUE # Returns with TOS to the caller of the function
这个结果中,我们反汇编了外层函数及其返回的闭包函数(为了便于查看,修改了部分⾏号)。从对上⾯两个函数的反汇编的注释可以⼤致了解闭包实现的步骤。python闭包中引⽤的⾃由变量实际存放在⼀个Cell对象中,当⾃由变元被闭包引⽤时,便将Cell中存放的⾃由变量的引⽤放⼊栈顶。
本例中Cell对象及其存放的⾃由变量分别为:
clo_func.func_closure[0] #Cell Object
clo_func.func_closure[0].cell_contents == 'local variable'# Free Variable
闭包实现的⼀个关键的地⽅是Cell Object,下⾯是官⽅给出的解释:
“Cell” objects are ud to implement variables referenced by multiple scopes. For each such variable, a cell object is created to store the value; the local variables of each stack frame that references the value contains a reference to the cells from outer scopes which also u that variable. When the value is accesd, the value contained in the cell is ud instead of the cell objec
t itlf. This de-referencing of the cell object requires support from the generated byte-code; the are not automatically de-referenced when accesd. Cell objects are not likely to be uful elwhere.
好了,限于篇幅就先介绍到这⾥。重要的是理解的基础上灵活的应⽤解决实际的问题并避免陷阱,希望本⽂能让你对闭包有⼀个不⼀样的认识。