Python是一门强大的动态语言,那么动态和强大在哪里呢?除了好的方面,Python的动态性是否隐藏了一些使用陷阱,有没有办法识别和避免它们?顺着它的动态特性这个话题,几篇文章依次探讨了:动态修改变量、动态定义函数、动态执行代码等。但是,当混合使用变量赋值、动态赋值、命名空间、作用域和函数时,事情才能真正得到编写原理等时很棘手。因此,本文结合前文的部分内容,进行延伸探讨,希望能厘清一些使用细节,更深入地探索Python语言的奥秘。(1)充满疑惑的例子先来看看这个例子:#Example0deffoo():exec('y=1+1')z=locals()['y']print(z)foo()#output:2exec()函数的代码块中定义了变量y,这个值可以被后面的locals()获取到,赋值后也打印出来。但是,仅根据此示例进行少量更改,结果可能会大不相同。#Example1deffoo():exec('y=1+1')y=locals()['y']print(y)foo()#Error:KeyError:'y'把前面例子中的z改成y,刚报错。其中,KeyError是指对应的key在字典中不存在。为什么会这样,无论新赋值的变量是y还是z,为什么对结果的影响如此不同?尝试去掉exec,不会报错!#例子2deffoo():y=1+1y=locals()['y']print(y)foo()#2问题:直接给y赋值,在exec()中动态赋值会不是locals()的值如何影响它?再次尝试给例1中的locals()赋值,还是报错:#例3deffoo():exec('y=1+1')boc=locals()y=boc['y']print(y)foo()#KeyError:'y'先做赋值,没用吗?不会的,调整赋值顺序到最前面就不会报错了:#Example4deffoo():boc=locals()exec('y=1+1')y=boc['y']print(y)foo()#2也就是说locals()的值不是固定的,它的值和调用的上下文有关,调用locals()的时机很关键。但是,如果要验证的话,在函数中添加一个locals()的print,这个动作会影响最终的执行结果。#示例5deffoo():boc=locals()exec('y=1+1')print(locals())y=boc['y']print(y)foo()#{'boc':{...}}#KeyError:'y'这是怎么回事?(2)多元知识的储备上面的例子在细微之处差别较大,主要受以下知识点的影响:1.变量的声明和赋值2.locals()取值和修改的逻辑3.locals()字典和局部命名空间的关系4.函数的编译,抽象语法树的解析注:exec()函数有两个默认参数globals()和locals()(与内置函数同名),以有限字符变量在字符串参数中的作用,如果加上,只会增加上面例子的复杂度,所以我们都做默认处理,这里讨论exec()只有一个参数的情况。在某些编程语言中,变量声明和赋值可以分开。比如声明的时候写inta,需要赋值的时候写a=1。当然也不一定要拆分,就是inta=1。对应到Python,情况就不同了。这两个动作在写的时候合二为一。首先,它不需要指定变量的类型,任何时候都不需要(也不可能)在变量前加一个类型(比如int)。第二,声明和赋值过程不能分开写,即只能写成a=1,其他语言看起来好像写成assignment,但实际上它的作用是inta=1。这虽然是一种方便,但也隐藏了一个难以察觉的陷阱(重点加了):当你看到a=1时,你无法判断a是第一次声明还是已经声明过。关于locals()的创建过程,locals()字典是本地命名空间的代理,它会收集本地范围内的变量。如果在代码运行时动态修改局部变量,只会影响字典,不会影响真正的局部变量。作用域变量。因此,当再次调用locals()时,动态修改的内容会因为重新获取而被丢弃。运行时本地命名空间是不可变的,这意味着exec()函数中的变量赋值不会影响它,但locals()字典是可变的,并且会受到exec()函数的影响。关于函数的编译,Python在编译时确定局部作用域内的合法变量名,然后在运行时绑定内容。范围内变量的解析与其执行顺序无关,更不用说是否会执行了。(3)薛定谔的猫以上内容为前提,友情提示,如有理解有歧义,请先阅读对应文章。以下是基于这些内容的分析。我不能保证每一个细节都准确无误,但是这个分析力求简单,全面,逻辑自洽,顺便幽默一下……在Example0中,虽然局部范围内没有'y',exec()函数是动态创建的,所以动态写入locals()字典中,所以可以找到而不报错。例1中exec()不影响局部作用域,即此时y还没有在局部作用域中声明赋值,下面这句是y在局部作用域中第一次声明赋值!y=locals()['y'],等号左边是在做声明,只要等号右边的结果为真,整个声明和赋值过程就为真。右边需要在locals()字典中查找y对应的值。当创建locals()字典时,由于变量y是在本地范围内声明的,我们首先在那里收集y而不必在exec()函数的动态结果中查找它。这个有一个字典的key,然后需要匹配这个key对应的value,也就是y绑定的value。不过刚才说了这是y的第一次赋值,还没有完成,所以y没有有效的绑定值。矛盾产生了,这里有点乱,先说清楚:左边的y是等待赋值完成,所以需要右边的执行结果;而右边的字典需要用到y的值,所以要依赖左边的y来完成赋值。双方的操作都没有完成,但双方都需要依赖对方先完成。这是一个牢不可破的僵局。可以说y的值是乱七八糟的,肯定等于“locals()['y']”,但是结果只能解开这组代码才能得到——只有打开笼子才知道结果,你会不会想起薛定谔的猫呢?locals()字典虽然获取到了y的名字,但是获取不到它的实际值,所以报了KeyError。例3也是一样,在赋值完成之前使用,所以报错。例2中,当y在二次赋值的过程中,本地命名空间中已经存在一个有效的等于2的y,所以locals()找到并使用它进行赋值,所以没有报错。至于Example4,和Example3只是执行顺序不同,为什么不报错呢?更奇怪的是,在Example4中再添加一个print(Example5)应该不会影响结果,但事实是又报错了,为什么呢?例4中boc=locals()这句也存在循环引用的问题,所以执行后字典中没有y,然后exec()这??句动态修改locals(),boc执行后的结果是{'y':2},所以下一句的boc['y']可以找到结果而不报错。Example4和Example3中的"y=boc['y']",虽然是第一次在局部作用域声明和赋值y,但是Example4中的boc已经被exec()修改过,所以可以得到Real值,不再有循环引用问题。再看例子5,第一个locals()还是有循环引用现象,然后exec()把变量y写入字典,但是第二个locals()触发了新的创建字典的过程,会把exec()被执行结果覆盖,于是进入第二轮循环引用,导致错误。示例5与示例4的不同之处在于,它是从本地作用域重新生成的字典,与示例3的效果相同。另外,请特别注意打印结果:{'boc':{…}}。这个结果说明第二个locals()是一个字典,它唯一的key是'boc','boc'映射到第一个locals()字典,也就是{…}。这种写法意味着里面有一个循环引用,从视觉上印证了前面所有的分析。词典内部出现循环引用,极为罕见!虽然前面已经分析过了,但是当你看到这里的时候,不知道你是不是觉得不可思议呢?之所以能记录第一次循环引用是因为我们没有尝试获取'y'的值,而第二次循环引用却因为值错误记录不下来。这个例子告诉大家:薛定谔的猫混进了Python字典,答案是打开笼子猫会死。字典的循环引用现象在几种情况下起着极其重要的作用,但往往被忽视。之所以不容易被注意到,是因为前面强调了一点:当你看到a=1的时候,你无法判断a是第一次声明还是已经声明过。这篇文章的KeyError其实是“局部变量'y'在赋值前被引用”,y已经定义但是没有赋值,导致引用报错。分配还是不分配,这是个问题。也是一只猫。最后,虽然这只猫在黑夜里搞得一团糟,但我们还是要感谢它:感谢它连接其他知识被我们“一锅端”,感谢它抓出一些生动活泼的东西为这篇抽象烧脑的文章活泼有趣……(还有,感谢它带来的标题灵感,不知有多少人为了标题而阅读?)以上就是本次分享的全部内容,如果想了解更多python知识,请前往公众号:Python编程学习圈,发“J”免费领取,每日干货分享
