洗礼灵魂,修炼python(23)--自定义函数(4)—闭包进阶问题—>报错UnboundLocalError: local variable 'x' referenced before assign

时间:2022-01-05 03:51:01

闭包(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)),为什么自己去研究>>> a20

 

再一例:

 洗礼灵魂,修炼python(23)--自定义函数(4)—闭包进阶问题—>报错UnboundLocalError: local variable 'x' referenced before assign

 

但是呢,有个很严重的问题,并不是外层的变量都可以调用的,如果是这样写的话:

def fun1(): x=5    def fun2(): x*=x return x return fun2

 结果:

 洗礼灵魂,修炼python(23)--自定义函数(4)—闭包进阶问题—>报错UnboundLocalError: local variable 'x' referenced before assign

问题来了:UnboundLocalError: local variable 'x' referenced before assignment

这是为嘛啊?看报错信息,提示的是本地变量x再调用前没有被定义,也就是说,fun2函数用的x并不是我们以为的fun1的变量,因为这里写法和上面的例子是不同的,那我改成一样的写法看看:

 洗礼灵魂,修炼python(23)--自定义函数(4)—闭包进阶问题—>报错UnboundLocalError: local variable 'x' referenced before assign

还是不行,为嘛啊?

我个人的理解是:在之前使用闭包是没有对传入的值进行改动的,是直接返回出来的,当我们传入4或者5时,传入这个举动就已经赋值定义好了X和y,这都是前一章参数的功劳。而后面的写法都是对传入的值有改变(x*=x),因为x指向的是一个不可变对象—常量,我们对传入的值已经改变,也就是内存ID发生改变,那么这个被改过的x*=3得到的X则是一个新的变量,在fun2函数空间内来说,这个X是没有被定义的,所以报错。而在之前我们只是声明了一个原始X的变量,所以返回时不报错

官方解释是:由于python对变量的搜索机制引起,当在调用函数时,如果有一个函数在内部对外层函数的变量做赋值操作 ,该变量会被认为是本地的(这就是前面说的屏蔽),所以如果你修改变量的值就会把其变成局部变量,在修改的同时就会创建一个同名的局部变量来屏蔽,那么该变量的引用自然就会出现未定义,所以这就解释为何直接调用不报错,而赋值操作就会报错

图解(个人理解):

 洗礼灵魂,修炼python(23)--自定义函数(4)—闭包进阶问题—>报错UnboundLocalError: local variable 'x' referenced before assign

 

那么这种情况怎么解决呢?在以后的开发中,总有要重新赋值的操作吧?是的,解决方法是有的,而且是多个方法:

方法一:使用nonlocal关键词

 洗礼灵魂,修炼python(23)--自定义函数(4)—闭包进阶问题—>报错UnboundLocalError: local variable 'x' referenced before assign

 

那么再直接以原写法加nonlocal关键词看看:

 洗礼灵魂,修炼python(23)--自定义函数(4)—闭包进阶问题—>报错UnboundLocalError: local variable 'x' referenced before assign

果然可行

注意:nonlocal参数在python3中才有,python2里没有

方法二:使用global关键词

使用global参数有两种情况:

1.已经有一个全局变量被定义,并且希望调用全局变量

洗礼灵魂,修炼python(23)--自定义函数(4)—闭包进阶问题—>报错UnboundLocalError: local variable 'x' referenced before assign

2.函数内外层都把对象定义为全局变量,并且希望调用外层函数的变量

洗礼灵魂,修炼python(23)--自定义函数(4)—闭包进阶问题—>报错UnboundLocalError: local variable 'x' referenced before assign

方法三:很“猥琐”的一招:建一个新的本地变量不要改动上层函数的变量

洗礼灵魂,修炼python(23)--自定义函数(4)—闭包进阶问题—>报错UnboundLocalError: local variable 'x' referenced before assign

是的,很猥琐,基本和外层函数没关系了,只不过值是一样的而已。顺便一说,虽然猥琐,但是确实是个办法,这样的操作在类定义里也会用到

方法四:使用容器类型

容器类型:如果某个对象包含对其他对象的引用,则将其称为容器。

容器不是存放在栈(后进先出的则为栈)里面,不受屏蔽影响,变量不会被屏蔽掉。字符串,列表,元祖都是容器类型

 好,那么我们试一下看看:

代码:

def fun1(): x=[5,6] def fun2(): x[0]*=x[0] return x[0] return fun2

结果是可行:

 洗礼灵魂,修炼python(23)--自定义函数(4)—闭包进阶问题—>报错UnboundLocalError: local variable 'x' referenced before assign

 

那么这个方法就会带来一个新的问题,先看看例子再说:

 洗礼灵魂,修炼python(23)--自定义函数(4)—闭包进阶问题—>报错UnboundLocalError: local variable 'x' referenced before assign

x并没有变对不对,这符合在前面说的,字符串,整形(常量)都是不可变对象,所以需要改则得重新赋值,这里要说明一下,变量本身是不可变对象,因为变量的赋值只是贴的一个标签而已,在前面的章节里说过的

那么使用容器类型呢?

 洗礼灵魂,修炼python(23)--自定义函数(4)—闭包进阶问题—>报错UnboundLocalError: local variable 'x' referenced before assign

 

果然是变了,因为list本身就是一个可变对象。但是说个很严重的问题,假如你在实际开发中,你的数据是list,这个数据很重要,你这样调用一次,list里的数据就变了,那么如果操作错或者说如果在测试中做测试,那岂不用一次少一次?这个时候,注意,你可以使用列表切片或者拷贝的方法来避免这种事发生

 

最后做个总结

  • 内部函数,不修改全局变量可以访问全局变量
  • 内部函数,修改同名全局变量,则python会认为它是一个局部变量
  • 在内部函数修改同名全局变量并调用变量,则引发UnboundLocalError: local variable 'x' referenced before assignment

 

看到这里,再结合前面的是不是有点懵X了?哈哈,没关系,多练习,多理解,很简单,你会看懂的