共计 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”即可收费获取,每日干货分享