本文原創並首發於公衆號【Python貓】,未經受權,請勿轉載。
原文地址:https://mp.weixin.qq.com/s/-fFVTgWVsydFsNu1nyxUzA編程
Python 是一門強大的動態語言,那動態體如今哪裏,強大又體如今哪裏呢?除了好的方面,Python 的動態性是否還藏着一些使用陷阱呢,有沒有辦法識別與避免呢?編程語言
沿着它的動態特性話題,貓哥有幾篇文章依次探及了:動態修改變量、動態定義函數、動態執行代碼等內容,然而,當混合了變量賦值、動態賦值、命名空間、做用域、函數的編譯原理等等內容時,問題就可能會變得很是棘手。函數
所以,這篇文章將前面一些內容融匯起來,再作一次延展的討論,但願可以理清一些使用的細節,更深刻地探索 Python 語言的奧祕。學習
先看看這一個例子:翻譯
# 例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'
這究竟是怎麼回事呢?
以上例子在細微之處有較大的不一樣,主要因爲如下知識點的影響:
一、變量的聲明與賦值
二、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 在編譯時就肯定了局部做用域內合法的變量名,在運行時再與內容綁定。做用域內變量的解析跟它的執行順序無關,更與是否會被執行無關。
以上內容是前提,友情提示,如你有理解模糊之處,請先閱讀對應的文章。接下來則是基於這些內容而做的分析。
我不敢保證每一個細節都準確無誤,但這個分析力求達到深刻淺出、面面俱到、邏輯自恰,並且順便幽默有趣……
例 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進階系列、好書推薦系列、技術寫做、優質英文推薦與翻譯等等,歡迎關注哦。後臺回覆「愛學習」,免費得到一份學習大禮包。