最近碰到这么个问题,有这么个函数,用来将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一个全局变量。(虽然好像没什么卵用。)