摘要:薛定諤的貓以上內(nèi)容是前提,友情提示,如你有理解模糊之處,請(qǐng)先閱讀對(duì)應(yīng)的文章。這個(gè)例子告訴大家薛定諤的貓混入了的字典中,而且答案是,打開籠子,這只貓就會(huì)死亡。
本文原創(chuàng)并首發(fā)于公眾號(hào)【Python貓】,未經(jīng)授權(quán),請(qǐng)勿轉(zhuǎn)載。
原文地址:https://mp.weixin.qq.com/s/-f...
Python 是一門強(qiáng)大的動(dòng)態(tài)語(yǔ)言,那動(dòng)態(tài)體現(xiàn)在哪里,強(qiáng)大又體現(xiàn)在哪里呢?除了好的方面,Python 的動(dòng)態(tài)性是否還藏著一些使用陷阱呢,有沒有辦法識(shí)別與避免呢?
沿著它的動(dòng)態(tài)特性話題,貓哥有幾篇文章依次探及了:動(dòng)態(tài)修改變量、動(dòng)態(tài)定義函數(shù)、動(dòng)態(tài)執(zhí)行代碼等內(nèi)容,然而,當(dāng)混合了變量賦值、動(dòng)態(tài)賦值、命名空間、作用域、函數(shù)的編譯原理等等內(nèi)容時(shí),問題就可能會(huì)變得非常棘手。
因此,這篇文章將前面一些內(nèi)容融匯起來(lái),再做一次延展的討論,希望能夠理清一些使用的細(xì)節(jié),更深入地探索 Python 語(yǔ)言的奧秘。
(1)疑惑重重的例子先看看這一個(gè)例子:
# 例0 def foo(): exec("y = 1 + 1") z = locals()["y"] print(z) foo() # 輸出:2
exec() 函數(shù)的代碼塊中定義了變量 y,這個(gè)值可以被隨后的 locals() 取到,在賦值后也打印了出來(lái)。然而,在這個(gè)例子的基礎(chǔ)上,只需做出小小的改變,結(jié)果就可能大不相同了。
# 例1 def foo(): exec("y = 1 + 1") y = locals()["y"] print(y) foo() # 報(bào)錯(cuò):KeyError: "y"
把前例的 z 改為 y ,就報(bào)錯(cuò)了。其中,KeyError 指的是在字典中不存在對(duì)應(yīng)的 key 。為什么會(huì)這樣呢,新賦值的變量是 y 或者 z,為什么對(duì)結(jié)果有這么不同的影響?
試試把 exec 去掉,不報(bào)錯(cuò)!
# 例2 def foo(): y = 1 + 1 y = locals()["y"] print(y) foo() # 2
問題:直接對(duì) y 賦值,跟動(dòng)態(tài)地在 exec() 中賦值,會(huì)對(duì) locals() 取值產(chǎn)生怎樣的影響?
再試試對(duì)例 1 的 locals() 先賦值,還是報(bào)錯(cuò):
# 例3 def foo(): exec("y = 1 + 1") boc = locals() y = boc["y"] print(y) foo() # KeyError: "y"
先做一次賦值,難道沒有用么?也不是,如果把賦值的順序調(diào)前,就不報(bào)錯(cuò)了:
# 例4 def foo(): boc = locals() exec("y = 1 + 1") y = boc["y"] print(y) foo() # 2
也就是說(shuō),locals() 的值并不是固定的,它的值與調(diào)用時(shí)的上下文相關(guān),調(diào)用 locals() 的時(shí)機(jī)至關(guān)重要。
然而,如果想要驗(yàn)證一下,在函數(shù)中增加一個(gè) locals() 的打印,這個(gè)動(dòng)作卻會(huì)影響到最終的執(zhí)行結(jié)果。
# 例5 def foo(): boc = locals() exec("y = 1 + 1") print(locals()) y = boc["y"] print(y) foo() # {"boc": {...}} # KeyError: "y"
這到底是怎么回事呢?
(2)多元知識(shí)的儲(chǔ)備以上例子在細(xì)微之處有較大的不同,主要由于以下知識(shí)點(diǎn)的影響:
1、變量的聲明與賦值
2、locals() 取值與修改的邏輯
3、locals() 字典與局部命名空間的關(guān)系
4、函數(shù)的編譯,抽象語(yǔ)法樹的解析
注意:exec() 函數(shù)有兩個(gè)缺省的參數(shù) globals() 與 locals() (與內(nèi)置函數(shù)同名),起的是限定字符串參數(shù)中變量的作用,若添加出來(lái),只會(huì)增加以上例子的復(fù)雜度,因此,我們都做缺省處理,這里討論的是 exec() 只有一個(gè)參數(shù)的情況。
在某些編程語(yǔ)言中,變量的聲明與賦值是可以分開的,例如在聲明時(shí)寫 int a ,需要賦值時(shí),再寫 a = 1 ,當(dāng)然也可不拆分,則是 int a = 1 。
對(duì)應(yīng)到 Python 中,情況就不同了,這兩個(gè)動(dòng)作在書寫時(shí)是合二為一的。首先它不用指定變量的類型,任何時(shí)候都不需要(也不能)在變量前加類型(如 int),其次,聲明與賦值過(guò)程無(wú)法拆分書寫,即只能寫成 a = 1 這樣。看起來(lái)它跟其它語(yǔ)言的賦值寫法一樣,但實(shí)際上,它的效果是 int a = 1 。
這雖然是一種便利,但也隱藏了一個(gè)不易察覺的陷阱(劃重點(diǎn)):當(dāng)看到 a = 1 時(shí),你無(wú)法確定 a 是初次聲明的,還是已被聲明過(guò)的。
關(guān)于 locals() 的創(chuàng)建過(guò)程,在《Python 動(dòng)態(tài)賦值的陷阱》文中有所分析,locals() 字典是局部命名空間的代理,它會(huì)采集局部作用域的變量,代碼運(yùn)行期若動(dòng)態(tài)修改局部變量,只會(huì)影響該字典,并不會(huì)影響真正的局部作用域的變量。因此,當(dāng)再次調(diào)用 locals() 時(shí),由于重新采集,則動(dòng)態(tài)修改的內(nèi)容會(huì)被丟棄。
運(yùn)行期的局部命名空間不可改變,這意味著 exec() 函數(shù)中的變量賦值不會(huì)對(duì)它產(chǎn)生影響,但 locals() 字典是可變的,會(huì)受到 exec() 函數(shù)的影響。
而關(guān)于函數(shù)的編譯,我在《Python與家國(guó)天下》中寫到了對(duì) 抽象語(yǔ)法樹 的分析,Python 在編譯時(shí)就確定了局部作用域內(nèi)合法的變量名,在運(yùn)行時(shí)再與內(nèi)容綁定。作用域內(nèi)變量的解析跟它的執(zhí)行順序無(wú)關(guān),更與是否會(huì)被執(zhí)行無(wú)關(guān)。
(3)薛定諤的貓以上內(nèi)容是前提,友情提示,如你有理解模糊之處,請(qǐng)先閱讀對(duì)應(yīng)的文章。接下來(lái)則是基于這些內(nèi)容而作的分析。
我不敢保證每個(gè)細(xì)節(jié)都準(zhǔn)確無(wú)誤,但這個(gè)分析力求達(dá)到深入淺出、面面俱到、邏輯自恰,而且順便幽默有趣……
例 0 中,局部作用域內(nèi)雖然沒有 ‘y’,但 exec() 函數(shù)動(dòng)態(tài)創(chuàng)建了它,因此動(dòng)態(tài)地寫入了 locals() 字典中,所以能查找到而不報(bào)錯(cuò)。
例 1 中,exec() 不影響局部作用域,即此時(shí) y 未在局部作用域內(nèi)做過(guò)聲明與賦值,接下來(lái)的一句才是第一次在局部作用域中對(duì) y 作聲明與賦值 !
y = locals()["y"] ,等號(hào)左側(cè)在做聲明,只要等號(hào)右側(cè)的結(jié)果成立,整個(gè)聲明與賦值的過(guò)程就成立。右側(cè)需在 locals() 字典中查找 y 對(duì)應(yīng)的值。
在創(chuàng)建 locals() 字典時(shí),由于局部作用域內(nèi)有變量 y 的聲明,因此我們首先在其中采集到了 y,而不必在 exec() 函數(shù)的動(dòng)態(tài)結(jié)果中查找。這就有了字典的一個(gè) key,接著要匹配這個(gè) key 對(duì)應(yīng)的值,也即 y 所綁定的值。
但是,剛才說(shuō)了這是 y 的第一次賦值,并未完成呢,因此 y 并無(wú)有效的綁定值。
矛盾出現(xiàn)了,這里有點(diǎn)繞,我們理一下:左側(cè)的 y 等著完成賦值,因此需要右側(cè)的執(zhí)行結(jié)果;而右側(cè)的字典需要使用到 y 的值,因此就依賴著左側(cè)的 y 完成賦值。兩邊的操作都未完成,但雙方都需要依賴對(duì)方先完成,這是個(gè)無(wú)法破解的死局。
可以說(shuō),y 的值是一團(tuán)混沌,它必然等于 “l(fā)ocals()["y"]” ,然而只有解開這團(tuán)代碼才能確切得到結(jié)果——只有打開籠子才知道結(jié)果,你是否想到了薛定諤的那只貓呢?
locals() 字典雖然拿到了 y 的名,卻拿不到它的實(shí),空歡喜一場(chǎng),所以報(bào) KeyError。
例 3 同理,未完成賦值就使用,所以報(bào)錯(cuò)。
例 2 中,y 在二次賦值的過(guò)程時(shí),局部命名空間中已經(jīng)存在著有效的 y 等于 2,因此 locals() 查找到它而用于賦值,所以不報(bào)錯(cuò)。
至于例 4,它跟例 3 只差了一個(gè)執(zhí)行順序,為什么不會(huì)報(bào)錯(cuò)呢?還有更奇怪的,在例 4 上再加一個(gè)打印(例5),理應(yīng)不會(huì)影響結(jié)果,可事實(shí)卻是又報(bào)錯(cuò)了,為什么?
例 4 中,boc = locals() 這句同樣存在循環(huán)引用的問題,因此執(zhí)行后的字典中沒有 y,接著 exec() 這句動(dòng)態(tài)地修改了 locals(),執(zhí)行后 boc 的結(jié)果是 {"y" : 2},因此再下一句的 boc["y"] 能查找到結(jié)果,而不報(bào)錯(cuò)。
例 4 與例 3 的 ”y = boc["y"]“ ,雖然都是第一次在局部作用域中聲明與賦值 y,但例 4 的 boc 已被 exec() 修改過(guò),因此它能取到實(shí)實(shí)在在的值,就不再有循環(huán)引用的問題了。
接著看例 5,第一個(gè) locals() 還是存在循環(huán)引用現(xiàn)象,接著 exec() 往字典中寫入變量 y,但是,第二個(gè) locals() 又觸發(fā)了新的創(chuàng)建字典過(guò)程,會(huì)把 exec() 的執(zhí)行結(jié)果覆蓋,因此進(jìn)入第二輪循環(huán)引用,導(dǎo)致報(bào)錯(cuò)。
例 5 與例 4 的不同在于,它是根據(jù)局部作用域重新生成的字典,其效果等同于例 3。
另外,請(qǐng)?zhí)貏e注意打印的結(jié)果:{"boc": {…}} 。
這個(gè)結(jié)果說(shuō)明,第二個(gè) locals() 是一個(gè)字典,而且它只有唯一的 key 是 ’boc‘,而 ’boc‘ 映射的是第一個(gè) locals() 字典,也即是 {...} 。這個(gè)寫法表示它內(nèi)部出現(xiàn)了循環(huán)引用,直觀地證實(shí)了前面的所有分析。
字典內(nèi)部出現(xiàn)循環(huán)引用 ,這個(gè)現(xiàn)象極其罕見!前面雖然做了分析,但看到這里的時(shí)候,不知道你是否覺得不可思議?
之所以第一次的循環(huán)引用能被記錄下來(lái),原因在于我們沒有試圖去取出 ’y‘ 的值,而第二個(gè)循環(huán)引用則由于取值報(bào)錯(cuò)而無(wú)法記錄下來(lái)。
這個(gè)例子告訴大家:薛定諤的貓混入了 Python 的字典中,而且答案是,打開籠子,這只貓就會(huì)死亡。
字典的循環(huán)引用現(xiàn)象在幾個(gè)例子中扮演了極其重要的角色,但是往往被人忽視。之所以難以被人覺察,原因還是前面劃重點(diǎn)的內(nèi)容:當(dāng)看到 a = 1 時(shí),你無(wú)法確定 a 是初次聲明的,還是已被聲明過(guò)的。
在《Python與家國(guó)天下》文中,貓哥分析了兩類經(jīng)典的報(bào)錯(cuò):name "x" is not defined、local variable "x" referenced before assignment。它們通常也是由于聲明與賦值不分,而導(dǎo)致的失察。
本文中的 KeyError 實(shí)際上就是 “l(fā)ocal variable "y" referenced before assignment”,y 已 defined 而未 assigned,導(dǎo)致 reference 時(shí)報(bào)錯(cuò)。
已賦值還是未賦值,這是個(gè)問題。也是一只貓。
最后,盡管這只貓?jiān)诎抵袚v了大亂,我們還是要感謝它:感謝它串聯(lián)了其它知識(shí)被我們“一鍋端”,感謝它為這篇抽象燒腦的文章?lián)铣隽藥追只顫娚鷦?dòng)的趣味……(以及,感謝它帶來(lái)的標(biāo)題靈感,不知道有多少人是沖著標(biāo)題而閱讀的?)
后記本文中的幾個(gè)例子早在 3 月 24 日就想到了,但我沒法給自己一套完全滿意的解答。在與群內(nèi)小伙伴們陸續(xù)討論了一整個(gè)下午后,我依然不滿足,最終打消了寫入《深度辨析 Python 的 eval() 與 exec()》這篇文章的念頭。兩個(gè)月來(lái),群內(nèi)偶爾討論過(guò)幾次相關(guān)的知識(shí)點(diǎn),感謝好幾位同學(xué)(特別@櫻雨樓)的討論,我終于覺得時(shí)機(jī)到了(其實(shí)是稿荒啦),把沉睡近兩個(gè)月的草稿翻出來(lái)……如今的分析,我自認(rèn)為是能說(shuō)得通,而且關(guān)鍵細(xì)節(jié)無(wú)遺漏的,但仍可能有瑕疵,如果你有什么想交流的,歡迎給我留言。
公眾號(hào)【Python貓】, 本號(hào)連載優(yōu)質(zhì)的系列文章,有喵星哲學(xué)貓系列、Python進(jìn)階系列、好書推薦系列、技術(shù)寫作、優(yōu)質(zhì)英文推薦與翻譯等等,歡迎關(guān)注哦。后臺(tái)回復(fù)“愛學(xué)習(xí)”,免費(fèi)獲得一份學(xué)習(xí)大禮包。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/43813.html
摘要:如果將小磁針看作神經(jīng)元,磁針狀態(tài)看作激發(fā)與抑制,也可以用來(lái)構(gòu)建深度學(xué)習(xí)的模型,或者玻爾茲曼機(jī)。這么多的基礎(chǔ)理論,展現(xiàn)了深度學(xué)習(xí)中的無(wú)處不在的物理本質(zhì)。 最近朋友圈里有大神分享薛定諤的滾,一下子火了,當(dāng)一個(gè)妹子叫你滾的時(shí)候,你永遠(yuǎn)不知道她是在叫你滾還是叫你過(guò)來(lái)抱緊,這確實(shí)是一種十分糾結(jié)的狀態(tài),而薛定諤是搞不清楚的,他連自己的貓是怎么回事還沒有弄清楚。雖然人們對(duì)于薛定諤頭腦中那只被放射性物質(zhì)殘害...
摘要:發(fā)布年月日,代號(hào)名稱為的發(fā)布正式版本。目前,最新的系列版本為年月日發(fā)布的版本,主要是對(duì)的安全更新。發(fā)布于年月初發(fā)布,代號(hào)薛定諤的貓,搭載和。 文章來(lái)源:程序員的資料庫(kù) Linux 和開源軟件在過(guò)去的一年里面都取得了不小的進(jìn)步。在這個(gè)特殊日子里面,我們對(duì) 2013 年這一年業(yè)界發(fā)生的重要事情分成了三個(gè)方面:Linux 發(fā)行版、重要周年慶祝活動(dòng)、曾引起業(yè)界較大關(guān)注的事件這三個(gè)方面進(jìn)行了梳理...
摘要:所以,我也想加一個(gè)打賞功能分析但在逛了一圈之后發(fā)現(xiàn),打賞插件基本上千篇一律的掃碼微信掃碼支付寶掃碼。但由于與微信無(wú)法直接喚醒,所以直接輸出一個(gè)與微信的二維碼,然后長(zhǎng)按掃碼實(shí)現(xiàn)支付。 前言 最近在逛博客時(shí),發(fā)現(xiàn)很多博客都帶了打賞功能,雖說(shuō)打賞的人可能很少,但始終是一份心意,能讓博主知道自己寫的文章有用,能夠幫助到人。所以,我也想加一個(gè)打賞功能~ 分析 但在github逛了一圈之后發(fā)現(xiàn),打...
閱讀 848·2023-04-25 23:59
閱讀 3737·2021-10-08 10:04
閱讀 1679·2019-08-30 14:05
閱讀 1015·2019-08-30 13:58
閱讀 489·2019-08-29 18:41
閱讀 1125·2019-08-29 17:15
閱讀 2318·2019-08-29 14:13
閱讀 2744·2019-08-29 13:27