理解Python的UnboundLocalError

今天写代码碰到一个百思不得解为什么会出错的代码,简化如下:

意图很明显,首先我定义了一个全局的x,在函数中,如果有特殊需要,就重新重新赋值一下x,否则就使用全局的x。

可以这段代码在运行的时候抛出这个Error:

UnboundLocalError: local variable ‘a’ referenced before assignment

研究了一番,觉得挺有意思的。而且这是一个比较常见的问题,在Stack Overflow的Python tag下面基本上是个周经问题。


出现赋值就是局部变量!

基本的原理很简单,在Python FAQ中提到了:

在Python中,如果变量仅仅是被引用而没有被赋值过,那么默认被视作全局变量。如果一个变量在函数中被赋值过,那么就被视作局部变量。

重点强调一下,这里的被赋值过,指的是在函数体内任何地方被赋值过。无论是否会被执行到(比如在if语句中),甚至是变量引用之后再赋值(参考下面的代码),都被作为“被赋值过”,都变成了局部变量。

其实到这里这个问题的答案已经出来了,只要是在函数体内被赋值过,那么变量就是local的,任何赋值之前的操作都会出现一个RuntimeError。下面会深入解释一下。

赋值操作的编译过程(原理)

Python文档中有关赋值语句提到:

Assignment of an object to a single target is recursively defined as follows. If the target is an identifier (name):

  • If the name does not occur in a global statement in the current code block: the name is bound to the object in the current local namespace.
  • Otherwise: the name is bound to the object in the current global namespace.

就是说,如果赋值操作的变量没有用global声明,那么就将这个name绑定到局部名字空间,否则就绑定到全局名字空间。

我们可以使用symtable这个lib验证一下:

可以看到,x变量确实被绑定到了局部。使用dis库可以看到编译的代码:

其中,LOAD_FAST是从local的stack中读取变量名(LOAD_FAST对之后字节码的优化很重要)。由此可以看到,的确是在局部变量找x没有找到(前面并没有STORE_FAST操作),引发了UnboundLocalError。

所以我的理解是:Python编译建立抽象语法树的时候,根据语法书建立符号表,从语法书的函数体内决定符号是local的还是global(是否出现assignment语句),然后在编译其他语句生成字节码。

那么既然这样,为什么要等到运行的时候才报错,而不是编译的时候就报错呢?

参考下面的代码:

如果something_true(),x的赋值就会执行,那么代码不会抛异常。但是编译器并不会知道这个赋值语句会不会执行。换句话说,函数体内出现了赋值语句,但是Python编译过程无法得知赋值语句会不会执行到的。所以只要出现了赋值语句,就将变量视为局部。至于会不会出现未赋值就使用(UnboundLocalError),就运行看看了。

解决方法

前面已经提到了,显示地指定使用global就可以,这样即使出现赋值,也不会产生作为local的变量,而是去改变global的变量。

但是依然存在一个问题:

external的x既不是local,也不是global。这种情况应该使用Python3的nonlocal

参考资料

Understanding UnboundLocalError in Python

Leave a comment

电子邮件地址不会被公开。 必填项已用*标注