第一次使用后重新分配局部变量时出现UnboundLocalError

以下代码在Python 2.5和3.0中都能正常工作:

a, b, c = (1, 2, 3)


print(a, b, c)


def test():
print(a)
print(b)
print(c)    # (A)
#c+=1       # (B)
test()

然而,当我取消注释行(B)时,我在行(一)处得到一个UnboundLocalError: 'c' not assignedab的值被正确打印。我不明白:

  1. 为什么在(一)行引发运行时错误,因为后面的语句在(B)行?

  2. 为什么变量ab按预期打印,而print(c)引发错误?

我能想到的唯一解释是赋值c+=1创建了一个当地的变量c,它优先于全局变量c。但是一个变量怎么能“偷”?存在之前的范围?为什么c显然是本地的?


另见在函数中使用全局变量用于简单地关于如何从函数中重新分配全局变量的问题,和是否有可能在python中修改变量,在外部,但不是全局,范围?用于从封闭函数(闭包)重新赋值。见为什么是't 'global'关键字需要访问全局变量?用于OP 期望错误但没有得到错误的情况,通过简单地访问没有global关键字的全局变量。

88062 次浏览

Python对函数中的变量的处理方式不同,这取决于你是在函数内部还是函数外部为变量赋值。如果变量是在函数中赋值的,默认情况下它被视为局部变量。因此,当你取消注释这一行时,你是在试图在给局部变量c赋值之前引用它。

如果你想让变量c引用在函数之前赋值的全局c = 3,请将

global c

作为函数的第一行。

至于python 3,现在有了

nonlocal c

可以用来引用最近的具有c变量的封闭函数作用域。

当您尝试传统的全局变量语义时,Python有相当有趣的行为。我不记得细节了,但你可以读取在'global'作用域中声明的变量的值,但如果你想修改它,你必须使用global关键字。尝试将test()改为这样:

def test():
global c
print(a)
print(b)
print(c)    # (A)
c+=1        # (B)

同样,你得到这个错误的原因是因为你也可以在函数中声明一个与“全局”同名的新变量,并且它将完全独立。解释器认为你试图在这个范围内创建一个名为c的新变量,并在一个操作中修改它,这在Python中是不允许的,因为这个新的c没有初始化。

Python有点奇怪,因为它将所有内容保存在不同作用域的字典中。原来的a b c在最上面的作用域,所以在最上面的字典里。该函数有自己的字典。当你到达print(a)print(b)语句时,字典中没有这个名字,所以Python会查找列表并在全局字典中找到它们。

现在我们得到c+=1,当然,它等价于c=c+1。当Python扫描这一行时,它说:“啊哈,有一个名为c的变量,我将把它放入我的本地作用域字典中。”然后,当它在赋值操作的右边寻找c的值时,它会找到它的局部变量c,它还没有值,因此抛出错误。

上面提到的global c语句只是告诉解析器它使用全局作用域的c,因此不需要新的。

它之所以说行中有问题是因为它在尝试生成代码之前有效地查找名称,因此在某种意义上,它认为它还没有真正地执行那一行。我认为这是一个可用性错误,但学习不认真对待编译器的消息通常是一个很好的实践。

如果这能让我感到安慰的话,我花了大约一天的时间来研究这个问题,然后我发现了Guido写的一些关于解释一切的字典的东西。

更新,见评论:

它不会扫描两次代码,但它确实分两个阶段扫描代码:词法分析和解析。

考虑一下如何解析这行代码。词法分析器读取源文本并将其分解为词素(语法的“最小组件”)。所以当它碰到直线时

c+=1

它把它分解成

SYMBOL(c) OPERATOR(+=) DIGIT(1)

解析器最终希望将其转换为解析树并执行它,但由于这是一个赋值操作,因此在执行之前,它会在本地字典中查找名称c,但没有看到它,并将其插入到字典中,并将其标记为未初始化。在完全编译的语言中,它只会进入符号表并等待解析,但由于它不会有第二次传递的奢侈,lexer会做一些额外的工作以使以后的工作更容易。然后它看到OPERATOR,看到规则说"如果你有一个OPERATOR +=左边一定已经初始化了"然后说"哎呀!"

这里的重点是它还没开始解析这行。这些都是实际解析前的准备工作,所以行计数器还没有执行到下一行。因此,当它发出错误信号时,它仍然认为它在上一行。

就像我说的,你可能会认为这是一个可用性漏洞,但实际上这是一件相当普遍的事情。有些编译器更诚实,会说“第XXX行附近有错误”,但这个编译器没有。

Python解释器将把函数作为一个完整的单元来读取。我认为它是分两次读取,一次是收集它的闭包(局部变量),然后再次将其转换为字节码。

我相信你已经意识到,在'='左边使用的任何名称都是隐式的局部变量。我不止一次因为改变一个变量对a +=的访问而被发现,它突然变成了一个不同的变量。

我还想指出的是,它与全局作用域并没有什么特别的关系。嵌套函数也有同样的行为。

看一看这个分解程序可能会清楚发生了什么:

>>> def f():
...    print a
...    print b
...    a = 1


>>> import dis
>>> dis.dis(f)


2           0 LOAD_FAST                0 (a)
3 PRINT_ITEM
4 PRINT_NEWLINE


3           5 LOAD_GLOBAL              0 (b)
8 PRINT_ITEM
9 PRINT_NEWLINE


4          10 LOAD_CONST               1 (1)
13 STORE_FAST               0 (a)
16 LOAD_CONST               0 (None)
19 RETURN_VALUE

如你所见,访问a的字节码是LOAD_FAST,访问b的字节码是LOAD_GLOBAL。这是因为编译器已经识别出a在函数中被赋值,并将其分类为局部变量。局部变量的访问机制与全局变量有本质上的不同——它们在帧的变量表中被静态地分配了一个偏移量,这意味着查找是一个快速的索引,而不是像全局变量那样更昂贵的字典查找。正因为如此,Python将print a行读取为“获取0号槽中本地变量'a'的值,并打印它”,当它检测到该变量仍未初始化时,会引发异常。

这不是对你的问题的直接回答,但它是密切相关的,因为这是由增广赋值和函数作用域之间的关系引起的另一个问题。

在大多数情况下,你倾向于认为增强赋值(a += b)完全等同于简单赋值(a = a + b)。不过,在一个角落的情况下,这可能会遇到一些麻烦。让我解释一下:

Python简单赋值的工作方式意味着,如果a被传递给一个函数(如func(a);注意Python总是引用传递),那么a = a + b将不会修改传入的a。相反,它只会修改指向a的局部指针。

但如果你使用a += b,那么它有时会被实现为:

a = a + b

或者有时(如果方法存在)为:

a.__iadd__(b)

在第一种情况下(只要a没有声明为全局的),在局部作用域之外没有副作用,因为对a的赋值只是一个指针更新。

在第二种情况下,a实际上会修改自身,因此所有对a的引用都指向修改后的版本。下面的代码演示了这一点:

def copy_on_write(a):
a = a + a
def inplace_add(a):
a += a
a = [1]
copy_on_write(a)
print a # [1]
inplace_add(a)
print a # [1, 1]
b = 1
copy_on_write(b)
print b # [1]
inplace_add(b)
print b # 1

因此,诀窍是避免对函数参数进行扩展赋值(我尝试只对局部/循环变量使用它)。使用简单的赋值,就不会出现模棱两可的行为。

这里有两个链接可能会有所帮助

1: =外地# why-am-i-getting-an-unboundlocalerror-when-the-variable-has-a-value docs.python.org/3.1/faq/programming.html ?亮点

2: =外地# how-do-i-write-a-function-with-output-parameters-call-by-reference docs.python.org/3.1/faq/programming.html ?亮点

link 1描述了错误UnboundLocalError。链接二可以帮助您重新编写测试函数。基于连杆二,原问题可以改写为:

>>> a, b, c = (1, 2, 3)
>>> print (a, b, c)
(1, 2, 3)
>>> def test (a, b, c):
...     print (a)
...     print (b)
...     print (c)
...     c += 1
...     return a, b, c
...
>>> a, b, c = test (a, b, c)
1
2
3
>>> print (a, b ,c)
(1, 2, 4)

最好的例子是:

bar = 42
def foo():
print bar
if False:
bar = 0

当调用foo()时,这也是提出了 UnboundLocalError,尽管我们永远不会到达bar=0行,所以逻辑上不应该创建局部变量。

其奥秘在于“Python是一种解释性语言”和函数foo的声明被解释为单个语句(即复合语句),它只是无声地解释它并创建局部和全局作用域。因此bar在执行前在局部作用域中被识别。

阅读这篇文章:http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

这篇文章提供了对Python变量作用域的完整描述和分析:

访问类变量的最佳方法是直接通过类名访问

class Employee:
counter=0


def __init__(self):
Employee.counter+=1

c+=1赋值c, python假设赋值的变量是本地的,但在这种情况下,它没有在本地声明。

使用globalnonlocal关键字。

nonlocal只适用于python 3,所以如果你正在使用python 2并且不想让你的变量为全局变量,你可以使用一个可变对象:

my_variables = { # a mutable object
'c': 3
}


def test():
my_variables['c'] +=1


test()

如果定义了与方法同名的变量,也可以得到此消息。

例如:

def teams():
...


def some_other_method():
teams = teams()

解决方案是将方法teams()重命名为诸如get_teams()之类的东西。

因为它只在本地使用,所以Python消息相当具有误导性!

你最终会得到这样的结果:

def teams():
...


def some_other_method():
teams = get_teams()

在初始化之后,通常在循环或条件块中对变量使用del关键字时,也会发生此问题。

与流行的观点相反,Python不是一个“被解释”的;任何有意义的语言。现在,这种情况已经非常罕见了。Python的参考实现以与Java或c#大致相同的方式编译Python代码:它被转换为虚拟机的操作码("bytecode"),然后进行模拟。其他实现也必须编译代码;否则,evalexec不能正确地返回对象,并且在不实际运行代码的情况下无法检测到SyntaxErrors。

在编译期间(无论是否在参考实现上),Python 遵循简单的规则用于决定函数中的变量作用域:

  • 如果函数包含一个名称的globalnonlocal声明,则该名称将分别被视为引用包含该名称的全局作用域或第一个封闭作用域。

  • 否则,如果它包含任何用于更改名称的绑定(赋值或删除)的语法,即使代码不会在运行时实际更改绑定,则名称为当地的

  • 否则,它要么指向包含该名称的第一个外围作用域,要么指向全局作用域。(当在全局作用域中查找名称时,内置作用域将作为运行时的回退检查;对内置作用域中的名称的赋值将在全局作用域中赋值。)

这里的重点是作用域是解析的在编译时。生成的字节码将直接指出在哪里查找。例如,在CPython 3.8中,有单独的操作码LOAD_CONST(编译时已知的常量)、LOAD_FAST (locals)、LOAD_DEREF(通过查找闭包来实现nonlocal查找,闭包实现为"cell"元组;LOAD_CLOSURE(在为嵌套函数创建的闭包对象中查找局部变量),以及LOAD_GLOBAL(在全局命名空间或内置命名空间中查找某些内容)。

没有“违约”;值。如果在查找它们之前它们还没有被分配,则会发生NameError。具体来说,对于本地查找,UnboundLocalError发生;这是NameError的子类型。


这里有一些重要的注意事项,请记住语法规则是在编译时实现的,使用无静态分析:

  • 如果永远无法到达代码,则返回没关系:
    y = 1
    def x():
    return y # local!
    if False:
    y = 0
    
  • 如果赋值将被优化为就地修改(例如扩展列表),则没关系 -从概念上讲,值仍然被赋值,这反映在参考实现中的字节码中,作为对同一对象的名称的无用重赋:
    y = []
    def x():
    y += [1] # local, even though it would modify `y` in-place with `global`
    
  • 然而,如果我们做一个索引/切片赋值,它会有的关系。(这将在编译时转换为不同的操作码,该操作码将依次调用__getitem__。)
    y = [0]
    def x():
    print(y) # global now! No error occurs.
    y[0] = 1
    
    还有其他形式的赋值,例如:
    y = 1
    def x():
    return y # local!
    for y in []:
    pass
    
  • 删除也改变了名称绑定,例如:
    y = 1
    def x():
    return y # local!
    del y
    

有兴趣的读者,使用参考实现,鼓励使用dis标准库模块检查这些例子。


对于globalnonlocal关键字,问题以同样的方式工作,加上必要的变更。(Python 2。x 没有nonlocal。)无论哪种方式,关键字都必须从外部作用域赋值给变量,但是对于只要查一下就行了是必要的,而对于变异这个查找对象也是必要的。(同样:列表上的+=会改变列表,但然后也会重新分配会将名称更改为相同的列表。)