当Python中混进一只薛定谔的猫……

本文原创并首发于公众号【Python猫】,未经受权,请勿转载。 原文地址:https://mp.weixin.qq.com/s/-fFVTgWVsydFsNu1nyxUzA编程

Python 是一门强大的动态语言,那动态体如今哪里,强大又体如今哪里呢?除了好的方面,Python 的动态性是否还藏着一些使用陷阱呢,有没有办法识别与避免呢?编程语言

沿着它的动态特性话题,猫哥有几篇文章依次探及了:动态修改变量、动态定义函数、动态执行代码等内容,然而,当混合了变量赋值、动态赋值、命名空间、做用域、函数的编译原理等等内容时,问题就可能会变得很是棘手。函数

所以,这篇文章将前面一些内容融汇起来,再作一次延展的讨论,但愿可以理清一些使用的细节,更深刻地探索 Python 语言的奥秘。学习

(1)疑惑重重的例子

先看看这一个例子:翻译

# 例0
def foo():
    exec('y = 1 + 1')
    z = locals()['y']
    print(z)
    
foo()

# 输出:2

exec() 函数的代码块中定义了变量 y,这个值能够被随后的 locals() 取到,在赋值后也打印了出来。然而,在这个例子的基础上,只需作出小小的改变,结果就可能大不相同了。代理

# 例1
def foo():
    exec('y = 1 + 1')
    y = locals()['y']
    print(y)
    
foo()

# 报错:KeyError: 'y'

把前例的 z 改成 y ,就报错了。其中,KeyError 指的是在字典中不存在对应的 key 。为何会这样呢,新赋值的变量是 y 或者 z,为何对结果有这么不一样的影响?code

试试把 exec 去掉,不报错!blog

# 例2
def foo():
    y = 1 + 1
    y = locals()['y']
    print(y)

foo()

# 2

问题:直接对 y 赋值,跟动态地在 exec() 中赋值,会对 locals() 取值产生怎样的影响?作用域

再试试对例 1 的 locals() 先赋值,仍是报错:字符串

# 例3
def foo():
    exec('y = 1 + 1')
    boc = locals()
    y = boc['y']
    print(y)
 
foo()

# KeyError: 'y'

先作一次赋值,难道没有用么?也不是,若是把赋值的顺序调前,就不报错了:

# 例4
def foo():
    boc = locals()
    exec('y = 1 + 1')
    y = boc['y']
    print(y)

foo()

# 2

也就是说,locals() 的值并非固定的,它的值与调用时的上下文相关,调用 locals() 的时机相当重要。

然而,若是想要验证一下,在函数中增长一个 locals() 的打印,这个动做却会影响到最终的执行结果。

# 例5
def foo():
    boc = locals()
    exec('y = 1 + 1')
    print(locals())
    y = boc['y']
    print(y)

foo()

# {'boc': {...}}
# KeyError: 'y'

这究竟是怎么回事呢?

(2)多元知识的储备

以上例子在细微之处有较大的不一样,主要因为如下知识点的影响:

一、变量的声明与赋值

二、locals() 取值与修改的逻辑

三、locals() 字典与局部命名空间的关系

四、函数的编译,抽象语法树的解析

注意:exec() 函数有两个缺省的参数 globals() 与 locals() (与内置函数同名),起的是限定字符串参数中变量的做用,若添加出来,只会增长以上例子的复杂度,所以,咱们都作缺省处理,这里讨论的是 exec() 只有一个参数的状况。

在某些编程语言中,变量的声明与赋值是能够分开的,例如在声明时写 int a ,须要赋值时,再写 a = 1 ,固然也可不拆分,则是 int a = 1

对应到 Python 中,状况就不一样了,这两个动做在书写时是合二为一的。首先它不用指定变量的类型,任什么时候候都不须要(也不能)在变量前加类型(如 int),其次,声明与赋值过程没法拆分书写,即只能写成 a = 1 这样。看起来它跟其它语言的赋值写法同样,但实际上,它的效果是 int a = 1

这虽然是一种便利,但也隐藏了一个不易察觉的陷阱(划重点):当看到 a = 1 时,你没法肯定 a 是初次声明的,仍是已被声明过的。

关于 locals() 的建立过程,在《Python 动态赋值的陷阱》文中有所分析,locals() 字典是局部命名空间的代理,它会采集局部做用域的变量,代码运行期若动态修改局部变量,只会影响该字典,并不会影响真正的局部做用域的变量。所以,当再次调用 locals() 时,因为从新采集,则动态修改的内容会被丢弃。

运行期的局部命名空间不可改变,这意味着 exec() 函数中的变量赋值不会对它产生影响,但 locals() 字典是可变的,会受到 exec() 函数的影响。

而关于函数的编译,我在《Python与家国天下》中写到了对 抽象语法树 的分析,Python 在编译时就肯定了局部做用域内合法的变量名,在运行时再与内容绑定。做用域内变量的解析跟它的执行顺序无关,更与是否会被执行无关。

(3)薛定谔的猫

以上内容是前提,友情提示,如你有理解模糊之处,请先阅读对应的文章。接下来则是基于这些内容而做的分析。

我不敢保证每一个细节都准确无误,但这个分析力求达到深刻浅出、面面俱到、逻辑自恰,并且顺便幽默有趣……

例 0 中,局部做用域内虽然没有 ‘y’,但 exec() 函数动态建立了它,所以动态地写入了 locals() 字典中,因此能查找到而不报错。

例 1 中,exec() 不影响局部做用域,即此时 y 未在局部做用域内作过声明与赋值,接下来的一句才是第一次在局部做用域中对 y 做声明与赋值

y = locals()['y'] ,等号左侧在作声明,只要等号右侧的结果成立,整个声明与赋值的过程就成立。右侧需在 locals() 字典中查找 y 对应的值。

在建立 locals() 字典时,因为局部做用域内有变量 y 的声明,所以咱们首先在其中采集到了 y,而没必要在 exec() 函数的动态结果中查找。这就有了字典的一个 key,接着要匹配这个 key 对应的值,也即 y 所绑定的值。

可是,刚才说了这是 y 的第一次赋值,并未完成呢,所以 y 并没有有效的绑定值。

矛盾出现了,这里有点绕,咱们理一下:左侧的 y 等着完成赋值,所以须要右侧的执行结果;而右侧的字典须要使用到 y 的值,所以就依赖着左侧的 y 完成赋值。两边的操做都未完成,但双方都须要依赖对方先完成,这是个没法破的死局。

能够说,y 的值是一团混沌,它必然等于 “locals()['y']” ,然而只有解开这团代码才能确切获得结果——只有打开笼子才知道结果,你是否想到了薛定谔的那只猫呢?

locals() 字典虽然拿到了 y 的名,却拿不到它的实,空欢喜一场,因此报 KeyError。

例 3 同理,未完成赋值就使用,因此报错。

例 2 中,y 在二次赋值的过程时,局部命名空间中已经存在着有效的 y 等于 2,所以 locals() 查找到它而用于赋值,因此不报错。

至于例 4,它跟例 3 只差了一个执行顺序,为何不会报错呢?还有更奇怪的,在例 4 上再加一个打印(例5),理应不会影响结果,可事实倒是又报错了,为何?

例 4 中,boc = locals() 这句一样存在循环引用的问题,所以执行后的字典中没有 y,接着 exec() 这句动态地修改了 locals(),执行后 boc 的结果是 {'y' : 2},所以再下一句的 boc['y'] 能查找到结果,而不报错。

例 4 与例 3 的 ”y = boc['y']“ ,虽然都是第一次在局部做用域中声明与赋值 y,但例 4 的 boc 已被 exec() 修改过,所以它能取到实实在在的值,就再也不有循环引用的问题了。

接着看例 5,第一个 locals() 仍是存在循环引用现象,接着 exec() 往字典中写入变量 y,可是,第二个 locals() 又触发了新的建立字典过程,会把 exec() 的执行结果覆盖,所以进入第二轮循环引用,致使报错。

例 5 与例 4 的不一样在于,它是根据局部做用域从新生成的字典,其效果等同于例 3。

另外,请特别注意打印的结果:{'boc': {…}}

这个结果说明,第二个 locals() 是一个字典,并且它只有惟一的 key 是 ’boc‘,而 ’boc‘ 映射的是第一个 locals() 字典,也便是 {...} 。这个写法表示它内部出现了循环引用,直观地证明了前面的全部分析。

字典内部出现循环引用 ,这个现象极其罕见!前面虽然作了分析,但看到这里的时候,不知道你是否以为难以想象?

之因此第一次的循环引用能被记录下来,缘由在于咱们没有试图去取出 ’y‘ 的值,而第二个循环引用则因为取值报错而没法记录下来。

这个例子告诉你们:薛定谔的猫混入了 Python 的字典中,并且答案是,打开笼子,这只猫就会死亡。

字典的循环引用现象在几个例子中扮演了极其重要的角色,可是每每被人忽视。之因此难以被人觉察,缘由仍是前面划重点的内容:当看到 a = 1 时,你没法肯定 a 是初次声明的,仍是已被声明过的。

在《Python与家国天下》文中,猫哥分析了两类经典的报错:name 'x' is not defined、local variable 'x' referenced before assignment。它们一般也是因为声明与赋值不分,而致使的失察。

本文中的 KeyError 实际上就是 “local variable 'y' referenced before assignment”,y 已 defined 而未 assigned,致使 reference 时报错。

已赋值仍是未赋值,这是个问题。也是一只猫。

最后,尽管这只猫在暗中捣了大乱,咱们仍是要感谢它:感谢它串联了其它知识被咱们“一锅端”,感谢它为这篇抽象烧脑的文章挠出了几分活泼生动的趣味……(以及,感谢它带来的标题灵感,不知道有多少人是冲着标题而阅读的?)

后记

本文中的几个例子早在 3 月 24 日就想到了,但我无法给本身一套彻底满意的解答。在与群内小伙伴们陆续讨论了一整个下午后,我依然不知足,最终打消了写入《深度辨析 Python 的 eval() 与 exec()》这篇文章的念头。两个月来,群内偶尔讨论过几回相关的知识点,感谢好几位同窗(特别@樱雨楼)的讨论,我终于以为时机到了(实际上是稿荒啦),把沉睡近两个月的草稿翻出来……现在的分析,我自认为是能说得通,并且关键细节无遗漏的,但仍可能有瑕疵,若是你有什么想交流的,欢迎给我留言。

公众号【Python猫】, 本号连载优质的系列文章,有喵星哲学猫系列、Python进阶系列、好书推荐系列、技术写做、优质英文推荐与翻译等等,欢迎关注哦。后台回复“爱学习”,免费得到一份学习大礼包。

相关文章
相关标签/搜索