当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,为什么对结果有这么不同的影响?
试试把 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() 的创建过程,在《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进阶系列、好书推荐系列、技术写作、优质英文推荐与翻译等等,欢迎关注哦。后台回复“爱学习”,免费获得一份学习大礼包。
当Python中混进一只薛定谔的猫……的更多相关文章
- windows10简单试用(多图,连薛定谔的猫都杀死了)
为了大家看起来方便,我的截图都是gif的,比较小,但是颜色会有色差,相信大家不介意的 昨天windows10可以下载第一时间就下了玩玩 由于是技术预览,所以不打算替换之前的系统,只装在虚拟机里玩玩就好 ...
- 【学习笔记】薛定谔的喵咪Cat—球盒问题(全详解)
[学习笔记]薛定谔的喵咪Cat-球盒问题(全详解) [题目描述] 当一个猫在盒子里时,因为放射物的状态我们不知道,所以猫的状态我们也不知道,这就所谓猫的生死纠缠态,也是所谓的薛定谔的猫. 当我们做需要 ...
- 年年出妖事,一例由JSON解析导致的"薛定谔BUG"排查过程记录
前言 做开发这么多年,也碰到无数的bug了.不过再复杂的bug,只要仔细去研读代码,加上debug,总能找到原因. 但是最近公司内碰到的这一个bug,这个bug初看很简单,但是非常妖孽,在一段时间内我 ...
- 只需一行代码!Python中9大时间序列预测模型
在时间序列问题上,机器学习被广泛应用于分类和预测问题.当有预测模型来预测未知变量时,在时间充当独立变量和目标因变量的情况下,时间序列预测就出现了. 预测值可以是潜在雇员的工资或银行账户持有人的信用评分 ...
- 三分钟搞定Python中的装饰器
python的装饰器是python的特色高级功能之一,言简意赅得说,其作用是在不改变其原有函数和类的定义的基础上,给他们增添新的功能. 装饰器存在的意义是什么呢?我们知道,在python中函数可以调用 ...
- 详解Python中的循环语句的用法
一.简介 Python的条件和循环语句,决定了程序的控制流程,体现结构的多样性.须重要理解,if.while.for以及与它们相搭配的 else. elif.break.continue和pass语句 ...
- 说说Python中的闭包 - Closure
转载自https://segmentfault.com/a/1190000007321972 Python中的闭包不是一个一说就能明白的概念,但是随着你往学习的深入,无论如何你都需要去了解这么一个东西 ...
- Python中的属性管理
Python管 理属性的方法一般有三种:操作符重载(即,__getattr__.__setattr__.__delattr__和 __getattribute__,有点类似于C++中的重载操作符).p ...
- 正则表达式学习与python中的应用
目录: 一.正则表达式的特殊符号 二.几种重要的正则表达式 三.python的re模块应用 四.参考文献 一.正则表达式的特殊符号 特殊符号可以说是正则表达式的关键,掌握并且可以灵活运用重要的pyth ...
随机推荐
- Hadoop-2.2.0中文文档—— MapReduce 下一代--容量调度器
目的 这份文档描写叙述 CapacityScheduler,一个为Hadoop能同意多用户安全地共享一个大集群的插件式调度器,如他们的应用能适时被分配限制的容量. 概述 CapacitySchedul ...
- 宿舍更换的新淋浴喷头"水温vs旋钮角度"关系的研究(曲线)
版权声明:我极少创造新知识,大部分情况下是个知识的二道贩子 https://blog.csdn.net/stereohomology/article/details/24478825 应该非常一目了然 ...
- SQL Server分区表,能否按照多个列作为分区函数的分区依据(转载)
问: Hi, I have a table workcachedetail with 40 million rows which has 8 columns.We decided to partiti ...
- 前端JSONPJIE解决跨域问题
解决同源策略的两个方法 1 . JSONP jsonp (将 JSON 数据填充进回调函数,这就是JSONP的JSON+Padding 的含义) jsonp是json用来跨域的一个东西,原理是通过sc ...
- ZOJ - 4016 Mergeable Stack 【LIST】
题目链接 http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=4016 题意 模拟栈的三种操作 第一种 push 将指定元素压入指 ...
- (linux)LED子系统
数据结构(/include/linux/leds.h) enum led_brightness { LED_OFF = 0, LED_HALF = 127, LED_FULL = 255, }; le ...
- android 内存泄漏问题【转】
本文转载自:http://www.voidcn.com/article/p-hbnuyfwz-ee.html 内存泄露问题在一些压力测试的场景很容易暴露,例如一些常用应用场景反复操作(eg:反复切换前 ...
- 版本名称SNAPSHOT、alpha、beta、release、GA含义
Alpha:是内部测试版,一般不向外部发布,会有很多Bug.一般只有测试人员使用.Beta:也是测试版,这个阶段的版本会一直加入新的功能.在Alpha版之后推出.RC:(Release Candida ...
- 为什么在实际的 kaggle 比赛中 gbdt 和 random forest 效果非常好?
https://www.zhihu.com/question/51818176/answer/127637712
- matlab之plot()函数
是个画图函数: 语法: figure(1000);hold on;plot(x,y);axis equal; 其中,x和y是某两个长度相同的列向量.比如:x=[1;2;3;4];y=[1;2;3;4] ...