當Python中混進一隻薛定諤的貓……

来源:https://www.cnblogs.com/pythonista/archive/2019/05/17/10883351.html
-Advertisement-
Play Games

本文原創並首發於公眾號【 Python貓 】,未經授權,請勿轉載。 原文地址:https://mp.weixin.qq.com/s/ fFVTgWVsydFsNu1nyxUzA Python 是一門強大的動態語言,那動態體現在哪裡,強大又體現在哪裡呢?除了好的方面,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進階系列、好書推薦系列、技術寫作、優質英文推薦與翻譯等等,歡迎關註哦。後臺回覆“愛學習”,免費獲得一份學習大禮包。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Recently, I was made a service which can provide a simple way to get best model. so, i spent lot of time to read source code of auto-sklearn, auto-skl ...
  • 1、JAVA 語言如何進行異常處理,關鍵字:throws,throw,try,catch,finally分別代表什麼意義?在try 塊中可以拋出異常嗎? 答:Java 通過面向對象的方法進行異常處理,把各種不同的異常進行分類,並提供了良好的介面。在Java 中,每個異常都是一個對象,它是Throwa ...
  • 第1題:動態載入又對及時性要求很高怎麼處理? 如何知道一個網站是動態載入的數據? 用火狐或者谷歌瀏覽器 打開你網頁,右鍵查看頁面源代碼,ctrl +F 查詢輸入內容,源代碼裡面並沒有這個值,說明是動態載入數據。 1. Selenium+Phantomjs 2. 儘量不使用 sleep 而使用 Web ...
  • 斐波那契數列、(引用float(‘inf’)無窮大的特性來比對,從而提取數組中的最小值)float(“inf”)正無窮大 float(“-inf”)負無窮大 ...
  • 本隨筆旨在強化理解傳值與傳引用 如下代碼的運行結果 其中i沒有改變,s也沒有改變。 但model中的值均改變了。 i :100s :hellomodel :testchangemodel2 :changeModel i :100s :hellomodel :testchangemodel2 :cha ...
  • 有時候maven真的很坑! 有時候提示invalid LOC header (bad signat signature), 但又有時候什麼都不提示,工程報錯,情況有肯多中,不知道大家遇到過幾種詭異的. ...
  • 之前寫了一個版本的,不過代碼繁瑣而且不好用,效率有些問題。尤其pdf轉圖片速度太慢。下麵是優化版本的代碼。 spriing_boot 版本信息:2.0.1.RELEASE 1、配置信息: application.yml 2、轉換入口 pdf 轉圖片參考 https://gitee.com/cycmy ...
  • 好的代碼風格,給人舒服的感覺,今天介紹一下谷歌的Python風格規範 1 分號 不要在行尾加分號, 也不要用分號將兩條命令放在同一行。 2 行長度 每行不超過80個字元;不要使用反斜杠連接行。Python會將圓括弧、中括弧和花括弧的行隱式的連接起來,可以利用這個特點。如果需要,可以在表達式外圍增加一 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...