关于python:当-Python-中混进一只薛定谔的猫……

11次阅读

共计 3660 个字符,预计需要花费 10 分钟才能阅读完成。

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,为什么对后果有这么不同的影响?

试试把 exec 去掉,不报错!

# 例 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)多元常识的储备
以上例子在轻微之处有较大的不同,次要因为以下知识点的影响:

1、变量的申明与赋值

2、locals() 取值与批改的逻辑

3、locals() 字典与部分命名空间的关系

4、函数的编译,形象语法树的解析

留神:exec() 函数有两个缺省的参数 globals() 与 locals()(与内置函数同名),起的是限定字符串参数中变量的作用,若增加进去,只会减少以上例子的复杂度,因而,咱们都做缺省解决,这里探讨的是 exec() 只有一个参数的状况。

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

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

这尽管是一种便当,但也暗藏了一个不易觉察的陷阱(划重点):当看到 a = 1 时,你无奈确定 a 是首次申明的,还是已被申明过的。

对于 locals() 的创立过程,locals() 字典是部分命名空间的代理,它会采集部分作用域的变量,代码运行期若动静批改局部变量,只会影响该字典,并不会影响真正的部分作用域的变量。因而,当再次调用 locals() 时,因为从新采集,则动静批改的内容会被抛弃。

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

对于函数的编译,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 是首次申明的,还是已被申明过的。

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

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

最初,只管这只猫在暗中捣了大乱,咱们还是要感激它:感激它串联了其它常识被咱们“一锅端”,感激它为这篇形象烧脑的文章挠出了几分活泼生动的趣味……(以及,感激它带来的题目灵感,不晓得有多少人是冲着题目而浏览的?)
以上就是本次分享的所有内容,想要理解更多 python 常识欢送返回公众号:Python 编程学习圈,发送“J”即可收费获取,每日干货分享

正文完
 0