最近碰到這麼個問題,有這麼個函數,用來將HTML轉義字符變回原來的字符:

其中unichr用來將一個整數轉換成Unicode字符,僅在Python2中存在。Python3中,chr可以同時處理ASCII字符和Unicode字符。所以我們在Python3環境中將unichr映射到chr上。

運行這段代碼會在第8行報錯:NameError: free variable ‘unichr’ referenced before assignment in enclosing scope。而且只有Python2會報錯,Python3不會。

首先從問題上看,報錯的原因是在閉包replc里unichr沒有定義。

但是Python2明明是有unichr這個內置函數的,為啥就變成未定義呢?

為了搞清楚問題,我們用了一個最小化的測試用例:

運行到print那行報錯“UnboundLocalError: local variable ‘d’ referenced before assignment”。我們注意到這時報錯的是local variable沒有定義。明明a之前是一個全局變量而且if根本不會執行啊。於是我們用dis模塊來打印func函數的字節碼:

確實python用了LOAD_FAST,說明print的是一個局部變量a。這看起來可能很難以理解,但其實大家一定碰到過下面這種情況:

這段代碼會在第三行報錯,同樣是UnboundLocalError。因為Python規定,在函數內僅被引用的變量默認為全局變量;如果在函數內被賦值,則默認為局部變量,除非有global關鍵詞。而且顯然,這個規則是在編譯(生成字節碼)時實現的,而不是在運行時確定的。

這個規則有人稱為LEGB規則 (Local, Enclosed, Global, Builtin),就是說依次從局部,閉包,全局,內置的命名空間里尋找名字。Python的編譯器在編譯時按照這一規則決定究竟從哪裡找變量。

在我們一開始的例子里,因為unichr在htmlescape這個局部作用域內出現了,所以Python認為可以從htmlescape的局部變量表裡取到它。但是在Python2上它未被賦值,所以出現了NameError。

有了這個設定,有時我們可以用它來unset一個全局變量。(雖然好像沒什麼卵用。)