最近碰到這麼個問題,有這麼個函數,用來將HTML轉義字符變回原來的字符:
1 2 3 4 5 6 7 8 9 10 11 12 |
def htmlescape(s): if sys.version_info[0] == 3: # python 3.x unichr = chr def replc(match): dict={'amp':'&','nbsp':' ','quot':'"','lt':'<','gt':'>','copy':'©','reg':'®'} if len(match.groups()) >= 2: if match.group(1) == '#': return unichr(int(match.group(2))) else: return dict.get(match.group(2), '?') htmlre = re.compile("&(#?)(\d{1,5}|\w{1,8}|[a-z]+);") return htmlre.sub(replc, s) |
其中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這個內置函數的,為啥就變成未定義呢?
為了搞清楚問題,我們用了一個最小化的測試用例:
1 2 3 4 5 |
a = 1 def func(): if False: a = 2 print(a) |
運行到print那行報錯“UnboundLocalError: local variable ‘d’ referenced before assignment”。我們注意到這時報錯的是local variable沒有定義。明明a之前是一個全局變量而且if根本不會執行啊。於是我們用dis模塊來打印func函數的字節碼:
1 2 3 4 5 6 7 8 9 10 11 12 |
3 0 LOAD_GLOBAL 0 (False) 3 POP_JUMP_IF_FALSE 15 4 6 LOAD_CONST 1 (2) 9 STORE_FAST 0 (a) 12 JUMP_FORWARD 0 (to 15) 5 >> 15 LOAD_FAST 0 (a) 18 PRINT_ITEM 19 PRINT_NEWLINE 20 LOAD_CONST 0 (None) 23 RETURN_VALUE |
確實python用了LOAD_FAST,說明print的是一個局部變量a。這看起來可能很難以理解,但其實大家一定碰到過下面這種情況:
1 2 3 4 |
a = 1 def func(): a = 2 print(a) |
這段代碼會在第三行報錯,同樣是UnboundLocalError。因為Python規定,在函數內僅被引用的變量默認為全局變量;如果在函數內被賦值,則默認為局部變量,除非有global關鍵詞。而且顯然,這個規則是在編譯(生成字節碼)時實現的,而不是在運行時確定的。
這個規則有人稱為LEGB規則 (Local, Enclosed, Global, Builtin),就是說依次從局部,閉包,全局,內置的命名空間里尋找名字。Python的編譯器在編譯時按照這一規則決定究竟從哪裡找變量。
在我們一開始的例子里,因為unichr在htmlescape這個局部作用域內出現了,所以Python認為可以從htmlescape的局部變量表裡取到它。但是在Python2上它未被賦值,所以出現了NameError。
有了這個設定,有時我們可以用它來unset一個全局變量。(雖然好像沒什麼卵用。)