对于该报错,典型的例子如下所示,对此网上的解释很多,但基本都是说,由于赋值把变量声明成了本地变量,但是本地并没有定义该变量,所以报错。解决办法是在本地通过global或nonlocal关键词将其声明为全局或者外层变量。这没有问题,只是这种解释不痛不痒,比较粗浅,没有进一步的分析原因。
x = 1
def f():
x += 1
print(x)
# f() --> raise UnboundLocalError
python程序的运行分为编译和执行,这里的编译指的是将源码编译成字节码,然后再由解释器解释执行。在编译阶段,编译器会对源码进行语法分析,在这个阶段,每个变量的所属的命名空间就会根据python的LEGB规则被确定,所以编译后的字节码实际上已经确定了变量的命名空间了。因此,当解释器执行字节码想要获取某个变量时,python会直接到其所属的命名空间中去获取,如果获取不到该变量(比如此时该变量还没有定义),便会报错。
下面例子中,尽管c的赋值操作时发生在该语句之后,但因为在解释器执行该程序之前的编译阶段,该变量被识别为本地变量,但是由于print(c)语句执行时,本地变量c还没有在本地定义,所以在本地的命名空间中并不存在,因此报错。所以下面的两个例子在第X行都会抛出同样的异常,即使赋值语句发送在第X行之后,甚至真正解释器执行的时候永远都不会执行到赋值语句。
a,b,c = 1,2,3
def f1():
print(a)
print(b)
print(c) # line X
c += 1
def f2():
print(a) # line X
if False:
a += 1
f1() # raise UnboundLocalError at line X
f2() # raise UnboundLocalError at line X
这可以解释有些人的部分困惑:解释器明明是按照文本顺序逐行解释的,为什么彷佛可以预知后面几行的赋值语句,提前抛出异常呢?现在我们知道,这其实是因为在解释之前的编译阶段,就已经给该变量确定了所属的命名空间。更准确的说,在字节码中,由于在语法分析阶段发现该变量在本地发生了被赋值操作,所以该变量已经被认为是本地变量,字节码里面会通过FAST来标识本地变量,所以解释器遇到FAST时,只会在本地的命名空间去找寻该变量,但是该变量并没有在本地初始化,因此,本地的命名空间中并没有该变量,那么就会抛出该异常,即本地变量没有绑定值,也就是没有初始化。
下面是对上述函数f1的字节码呈现,在编译的语法分析阶段,会将f1函数编译成下面的字节码,可以发现,对于a,b,都将其编译成全局变量,以LOAD_GLOBAL标识;但是对于c,在print(c)语句中,c的导入是LOAD_FAST,表示其已经是本地变量了。
dis.dis(f1)
4 0 LOAD_GLOBAL 0 (print)
2 LOAD_GLOBAL 1 (a)
4 CALL_FUNCTION 1
6 POP_TOP
5 8 LOAD_GLOBAL 0 (print)
10 LOAD_GLOBAL 2 (b)
12 CALL_FUNCTION 1
14 POP_TOP
6 16 LOAD_GLOBAL 0 (print)
18 LOAD_FAST 0 (c)
20 CALL_FUNCTION 1
22 POP_TOP
7 24 LOAD_FAST 0 (c)
26 LOAD_CONST 1 (1)
28 INPLACE_ADD
30 STORE_FAST 0 (c)
32 LOAD_CONST 0 (None)
34 RETURN_VALUE
如果我们在代码中,通过global关键字显式的将c声明为全局变量,那么在语法分析时,就会将c认为时全局变量,解释器便会在全局命名空间中去寻找该变量。
对于全局变量,我们可以通过globals()去获取,其返回一个字典,不可修改,key就是全局变量名,value是对应的值。同理,本地变量可以通过locals()获取。这里的globals()和locals()返回的变量都是可以访问的已经初始化了的变量,未初始化的变量不可见,不会存在其中。
如果我们在一个函数中通过global关键字声明了一个之前不存在的变量x会怎么样呢?实际上同理,在编译阶段,将其标识为全局变量,但是全局命名空间中不存在,引用会报错,但对于全局变量,不是报UnboundLocalError异常,而是NameError:name 'x' is not defined,该变量名未定义。
python中可以通过gloabl、nonlocal来将变量的命名空间声明为全局和外层的,那么哪些操作会将变量声明为本地变量呢?本文列出如下场景,尽量在代码中注意到这些点,以避免再犯类似错误:
1. 对于一切的赋值操作,都会将变量声明为本地变量;
2. 对于不可变对象,一切改变对象的操作,也会将其声明为本地变量,比如赋值语句或del x语句;
3. 本地import x,x也会被声明为本地变量;
4. 本地的异常捕捉中对异常重新命名,如try...except Exception as e,尽管没有异常,但是该语句已经将e声明为了本地变量,因此在此之前,对e的引用被认为是本地变量e的引用;此外,对于异常捕捉中新命名的变量e,需要额外注意,一旦抛出异常被捕捉后进入捕捉后的except代码块,e相对于该代码块是本地局部变量,一旦跳出该代码块,e会从本地命名空间中被删除,从而后续本地代码再引用e,会抛出UnboundLocalError异常,但是如果没有进入except代码块便不会被删除;这属于3.x的新特性,在2.x中,e会一直被保留。具体如下所示,调用该函数,执行到第x行,会抛出UnboundLocalError异常,原因是解释器从上面的print(e)跳出来后,e就被删除了,所以下面的print(e)中的e变成了未初始化的本地变量,因为无法在本地命名空间中找到。但是在2.x中,不会删除,所以代码可以正常运行。
def f():
e=1
print(e)
try:
x=0
b = 1/x
except Exception as e:
print(e)
print(e) # line X
5. for x in iterable语句中,会将x声明为本地变量,所以如果之前预期引用了全局的x变量,由于后面的for loop会将x声明为本地变量,因此会抛出UnboundLocalError异常;
6. 2.x中的列表推导式[x for x in iterable]会将x声明为本地变量,同样若执行上述相似操作,会抛出异常;在2.x中,列表推导式的变量x会一致在本地有效;但是在3.x中,列表推导式会拥有一个自己的作用域,x只会存在在列表推导式的作用域中,不会对外层代码有所影响,因此,在3.x中,列表推导式不会将x声明为本地变量,也不会对后续代码有影响;但是无论是2.x还是3.x,元组推导式(生成器推导式)以及字典推导式都和3.x中列表推导式的行为一样,拥有自身的作用域,不会对外层代码造成影响。