闭包(lexical closure)
什么是闭包前面已经说过了,但是由于遗留问题,所以单独作为一个章节详解讲解下
不多说,看例子:
def funx(x): def funy(y): return x*y #此时的funy函数对外层funx函数的变量调用,则称为闭包 return funy
结果:
>>> i=funx(4) >>> i <function funx.<locals>.funy at 0x000000000331B7B8> >>> i(5) 20 >>> a=funx(4)(5) #切不可写成a=funx(4(5)) >>> a 20
再一例:
但是呢,有个很严重的问题,并不是外层的变量都可以调用的,如果是这样写的话:
def fun1(): x=5 def fun2(): x*=x return x return fun2
结果:
问题来了:UnboundLocalError: local variable 'x' referenced before assignment
这是为嘛啊?看报错信息,提示的是本地变量x再调用前没有被定义,也就是说,fun2函数用的x并不是我们以为的fun1的变量,因为这里写法和上面的例子是不同的,那我改成一样的写法看看:
还是不行,为嘛啊?
我个人的理解是:在之前使用闭包是没有对传入的值进行改动的,是直接返回出来的,当我们传入4或者5时,传入这个举动就已经赋值定义好了X和y,这都是前一章参数的功劳。而后面的写法都是对传入的值有改变(x*=x),因为x指向的是一个不可变对象—常量,我们对传入的值已经改变,也就是内存ID发生改变,那么这个被改过的x*=3得到的X则是一个新的变量,在fun2函数空间内来说,这个X是没有被定义的,所以报错。而在之前我们只是声明了一个原始X的变量,所以返回时不报错
官方解释是:由于python对变量的搜索机制引起,当在调用函数时,如果有一个函数在内部对外层函数的变量做赋值操作 ,该变量会被认为是本地的(这就是前面说的屏蔽),所以如果你修改变量的值就会把其变成局部变量,在修改的同时就会创建一个同名的局部变量来屏蔽,那么该变量的引用自然就会出现未定义,所以这就解释为何直接调用不报错,而赋值操作就会报错
图解(个人理解):
那么这种情况怎么解决呢?在以后的开发中,总有要重新赋值的操作吧?是的,解决方法是有的,而且是多个方法:
方法一:使用nonlocal关键词
那么再直接以原写法加nonlocal关键词看看:
果然可行
注意:nonlocal参数在python3中才有,python2里没有
方法二:使用global关键词
使用global参数有两种情况:
1.已经有一个全局变量被定义,并且希望调用全局变量
2.函数内外层都把对象定义为全局变量,并且希望调用外层函数的变量
方法三:很“猥琐”的一招:建一个新的本地变量不要改动上层函数的变量
是的,很猥琐,基本和外层函数没关系了,只不过值是一样的而已。顺便一说,虽然猥琐,但是确实是个办法,这样的操作在类定义里也会用到
方法四:使用容器类型
容器类型:如果某个对象包含对其他对象的引用,则将其称为容器。
容器不是存放在栈(后进先出的则为栈)里面,不受屏蔽影响,变量不会被屏蔽掉。字符串,列表,元祖都是容器类型
好,那么我们试一下看看:
代码:
def fun1(): x=[5,6] def fun2(): x[0]*=x[0] return x[0] return fun2
结果是可行:
那么这个方法就会带来一个新的问题,先看看例子再说:
x并没有变对不对,这符合在前面说的,字符串,整形(常量)都是不可变对象,所以需要改则得重新赋值,这里要说明一下,变量本身是不可变对象,因为变量的赋值只是贴的一个标签而已,在前面的章节里说过的
那么使用容器类型呢?
果然是变了,因为list本身就是一个可变对象。但是说个很严重的问题,假如你在实际开发中,你的数据是list,这个数据很重要,你这样调用一次,list里的数据就变了,那么如果操作错或者说如果在测试中做测试,那岂不用一次少一次?这个时候,注意,你可以使用列表切片或者拷贝的方法来避免这种事发生
最后做个总结:
- 内部函数,不修改全局变量可以访问全局变量
- 内部函数,修改同名全局变量,则python会认为它是一个局部变量
- 在内部函数修改同名全局变量并调用变量,则引发UnboundLocalError: local variable 'x' referenced before assignment
看到这里,再结合前面的是不是有点懵X了?哈哈,没关系,多练习,多理解,很简单,你会看懂的