首次使用后重新分配时,局部变量上出现UnboundLocalError

问题:首次使用后重新分配时,局部变量上出现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 assigned(A)行得到了注释。的值ab被正确地打印。这使我完全困惑,原因有两个:

  1. 为什么由于行(B)的后面的语句而在行(A)抛出运行时错误?

  2. 为什么在按预期方式打印变量a并产生错误?bc

我能提出的唯一解释是,局部变量c是由赋值创建的c+=1,它c甚至在创建局部变量之前就优先于“全局”变量。当然,在变量存在之前“窃取”范围是没有意义的。

有人可以解释这种现象吗?

The following code works as expected in both Python 2.5 and 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()

However, when I uncomment line (B), I get an UnboundLocalError: 'c' not assigned at line (A). The values of a and b are printed correctly. This has me completely baffled for two reasons:

  1. Why is there a runtime error thrown at line (A) because of a later statement on line (B)?

  2. Why are variables a and b printed as expected, while c raises an error?

The only explanation I can come up with is that a local variable c is created by the assignment c+=1, which takes precedent over the “global” variable c even before the local variable is created. Of course, it doesn’t make sense for a variable to “steal” scope before it exists.

Could someone please explain this behavior?


回答 0

Python对函数中的变量的处理方式有所不同,具体取决于您是从函数内部还是外部为其分配值。如果在函数中分配了变量,则默认情况下会将其视为局部变量。因此,当取消注释该行时,您将尝试c在分配任何值之前引用该局部变量。

如果要让变量c引用c = 3该函数之前分配的全局变量,请输入

global c

作为函数的第一行。

至于python 3,现在有

nonlocal c

您可以用来引用最近的包含c变量的封闭函数范围。

Python treats variables in functions differently depending on whether you assign values to them from inside or outside the function. If a variable is assigned within a function, it is treated by default as a local variable. Therefore, when you uncomment the line you are trying to reference the local variable c before any value has been assigned to it.

If you want the variable c to refer to the global c = 3 assigned before the function, put

global c

as the first line of the function.

As for python 3, there is now

nonlocal c

that you can use to refer to the nearest enclosing function scope that has a c variable.


回答 1

Python有点怪异之处在于,它把所有内容都保存在字典中以适应各种范围。原始的a,b,c在最高范围内,因此在该最高字典中。该函数具有其自己的字典。当到达print(a)and print(b)语句时,字典中没有该名称的任何内容,因此Python查找列表并在全局字典中找到它们。

现在我们到达c+=1,它当然等于c=c+1。当Python扫描该行时,它说:“啊哈,有一个名为c的变量,我将其放入本地范围字典中。” 然后,当它在赋值的右侧为c寻找c的值时,会找到名为c的局部变量,该变量尚无值,因此引发错误。

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

它之所以说在线上存在问题,是因为它在尝试生成代码之前就在有效地寻找名称,因此从某种意义上说,它还没有真正做到这一点。我认为这是一个可用性错误,但是通常最好的做法是只学习不要过于重视编译器的消息。

如果可以的话,我可能花了一天的时间来研究和试验相同的问题,然后才发现Guido写了一些有关解释一切的字典的东西。

更新,请参阅评论:

它不会扫描代码两次,但是会在两个阶段(词法分析和解析)中扫描代码。

考虑一下这一行代码的解析方式。词法分析器读取源文本并将其分解为词素,即语法的“最小组件”。所以当它到达终点时

c+=1

它分解成类似

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

解析器最终希望将其放入解析树中并执行它,但是由于它是一个赋值,因此在解析树之前,它会在本地字典中查找名称c,没有看到它,然后将其插入字典中,它未初始化。用完全编译的语言,它将只进入符号表并等待解析,但是由于它没有第二遍的奢侈,因此词法分析器做了一些额外的工作以使以后的生活更轻松。仅然后,它看到操作员,看到规则说“如果您有操作员+ =,则必须已经初始化了左侧”,并说“哇!”

这里的要点是它还没有真正开始解析该行。这一切都是为实际解析做准备,因此行计数器尚未前进到下一行。因此,当它发出错误信号时,它仍会在前一行上考虑它。

正如我所说,您可能会争辩说这是一个可用性错误,但这实际上是相当普遍的事情。一些编译器对此更为诚实,并说“ XXX行或其附近的错误”,但事实并非如此。

Python is a little weird in that it keeps everything in a dictionary for the various scopes. The original a,b,c are in the uppermost scope and so in that uppermost dictionary. The function has its own dictionary. When you reach the print(a) and print(b) statements, there’s nothing by that name in the dictionary, so Python looks up the list and finds them in the global dictionary.

Now we get to c+=1, which is, of course, equivalent to c=c+1. When Python scans that line, it says “aha, there’s a variable named c, I’ll put it into my local scope dictionary.” Then when it goes looking for a value for c for the c on the right hand side of the assignment, it finds its local variable named c, which has no value yet, and so throws the error.

The statement global c mentioned above simply tells the parser that it uses the c from the global scope and so doesn’t need a new one.

The reason it says there’s an issue on the line it does is because it is effectively looking for the names before it tries to generate code, and so in some sense doesn’t think it’s really doing that line yet. I’d argue that is a usability bug, but it’s generally a good practice to just learn not to take a compiler’s messages too seriously.

If it’s any comfort, I spent probably a day digging and experimenting with this same issue before I found something Guido had written about the dictionaries that Explained Everything.

Update, see comments:

It doesn’t scan the code twice, but it does scan the code in two phases, lexing and parsing.

Consider how the parse of this line of code works. The lexer reads the source text and breaks it into lexemes, the “smallest components” of the grammar. So when it hits the line

c+=1

it breaks it up into something like

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

The parser eventually wants to make this into a parse tree and execute it, but since it’s an assignment, before it does, it looks for the name c in the local dictionary, doesn’t see it, and inserts it in the dictionary, marking it as uninitialized. In a fully compiled language, it would just go into the symbol table and wait for the parse, but since it WON’T have the luxury of a second pass, the lexer does a little extra work to make life easier later on. Only, then it sees the OPERATOR, sees that the rules say “if you have an operator += the left hand side must have been initialized” and says “whoops!”

The point here is that it hasn’t really started the parse of the line yet. This is all happening sort of preparatory to the actual parse, so the line counter hasn’t advanced to the next line. Thus when it signals the error, it still thinks its on the previous line.

As I say, you could argue it’s a usability bug, but its actually a fairly common thing. Some compilers are more honest about it and say “error on or around line XXX”, but this one doesn’t.


回答 2

看一下反汇编可以澄清正在发生的事情:

>>> 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分配给它,并将其归类为局部变量。局部变量的访问机制与全局变量的根本不同-它们在帧的变量表中静态分配了一个偏移量,这意味着查找是一个快速索引,而不是全局变量更昂贵的dict查找。因此,Python将print a行读为“获取插槽0中保存的局部变量’a’的值,并打印出来”,并且当它检测到该变量仍未初始化时,将引发异常。

Taking a look at the disassembly may clarify what is happening:

>>> 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

As you can see, the bytecode for accessing a is LOAD_FAST, and for b, LOAD_GLOBAL. This is because the compiler has identified that a is assigned to within the function, and classified it as a local variable. The access mechanism for locals is fundamentally different for globals – they are statically assigned an offset in the frame’s variables table, meaning lookup is a quick index, rather than the more expensive dict lookup as for globals. Because of this, Python is reading the print a line as “get the value of local variable ‘a’ held in slot 0, and print it”, and when it detects that this variable is still uninitialised, raises an exception.


回答 3

当您尝试传统的全局变量语义时,Python具有相当有趣的行为。我不记得详细信息,但是您可以很好地读取在“全局”范围内声明的变量的值,但是如果要修改它,则必须使用global关键字。尝试更改test()为此:

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

另外,出现此错误的原因是因为您还可以在该函数内声明一个新变量,其名称与“全局”变量相同,因此它将是完全独立的。解释器认为您正在尝试在此范围内创建一个新变量,c并在一个操作中对其进行全部修改,这在Python中是不允许的,因为c未初始化此新变量。

Python has rather interesting behavior when you try traditional global variable semantics. I don’t remember the details, but you can read the value of a variable declared in ‘global’ scope just fine, but if you want to modify it, you have to use the global keyword. Try changing test() to this:

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

Also, the reason you are getting this error is because you can also declare a new variable inside that function with the same name as a ‘global’ one, and it would be completely separate. The interpreter thinks you are trying to make a new variable in this scope called c and modify it all in one operation, which isn’t allowed in Python because this new c wasn’t initialized.


回答 4

清楚说明的最佳示例是:

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

在调用时foo(),尽管我们永远都不会到达line ,但这也会引发问题 ,因此从逻辑上讲,绝对不应创建局部变量。UnboundLocalErrorbar=0

神秘之处在于“ Python是一种解释性语言 ”,并且函数的声明foo被解释为单个语句(即复合语句),它只是笨拙地解释它并创建局部和全局作用域。因此bar在执行之前会在本地范围内被识别。

有关此类的更多示例,请阅读以下文章:http : //blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

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

The best example that makes it clear is:

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

when calling foo() , this also raises UnboundLocalError although we will never reach to line bar=0, so logically local variable should never be created.

The mystery lies in “Python is an Interpreted Language” and the declaration of the function foo is interpreted as a single statement (i.e. a compound statement), it just interprets it dumbly and creates local and global scopes. So bar is recognized in local scope before execution.

For more examples like this Read this post: http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

This post provides a Complete Description and Analyses of the Python Scoping of variables:


回答 5

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

1:当变量具有值时,docs.python.org / 3.1 / faq / programming.html?highlight = nonlocal#why-am-i-getting-unboundboundlocalerror-

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

链接一描述了错误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)

Here are two links that may help

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

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

link one describes the error UnboundLocalError. Link two can help with with re-writing your test function. Based on link two, the original problem could be rewritten as:

>>> 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)

回答 6

这不是您问题的直接答案,而是紧密相关的,因为这是由扩展分配和函数作用域之间的关系引起的另一个难题。

在大多数情况下,您倾向于认为扩充分配(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

因此,诀窍是避免在函数参数上增加分配(我尝试仅将其用于局部/循环变量)。使用简单的分配,您就可以避免歧义行为。

This is not a direct answer to your question, but it is closely related, as it’s another gotcha caused by the relationship between augmented assignment and function scopes.

In most cases, you tend to think of augmented assignment (a += b) as exactly equivalent to simple assignment (a = a + b). It is possible to get into some trouble with this though, in one corner case. Let me explain:

The way Python’s simple assignment works means that if a is passed into a function (like func(a); note that Python is always pass-by-reference), then a = a + b will not modify the a that is passed in. Instead, it will just modify the local pointer to a.

But if you use a += b, then it is sometimes implemented as:

a = a + b

or sometimes (if the method exists) as:

a.__iadd__(b)

In the first case (as long as a is not declared global), there are no side-effects outside local scope, as the assignment to a is just a pointer update.

In the second case, a will actually modify itself, so all references to a will point to the modified version. This is demonstrated by the following code:

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

So the trick is to avoid augmented assignment on function arguments (I try to only use it for local/loop variables). Use simple assignment, and you will be safe from ambiguous behaviour.


回答 7

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

如您所知,您可能已经知道,在’=’左边使用的任何名称都隐含一个局部变量。我不止一次地通过将变量访问更改为+ =被发现,这突然是一个不同的变量。

我还想指出,这实际上与全局范围无关。嵌套函数具有相同的行为。

The Python interpreter will read a function as a complete unit. I think of it as reading it in two passes, once to gather its closure (the local variables), then again to turn it into byte-code.

As I’m sure you were already aware, any name used on the left of a ‘=’ is implicitly a local variable. More than once I’ve been caught out by changing a variable access to a += and it’s suddenly a different variable.

I also wanted to point out it’s not really anything to do with global scope specifically. You get the same behaviour with nested functions.


回答 8

c+=1Assigns c,python假定已分配的变量是局部变量,但在这种情况下,尚未在本地声明。

使用globalnonlocal关键字。

nonlocal 仅在python 3中有效,因此,如果您使用的是python 2,并且不想将变量设置为全局变量,则可以使用可变对象:

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

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

test()

c+=1 assigns c, python assumes assigned variables are local, but in this case it hasn’t been declared locally.

Either use the global or nonlocal keywords.

nonlocal works only in python 3, so if you’re using python 2 and don’t want to make your variable global, you can use a mutable object:

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

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

test()

回答 9

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

class Employee:
    counter=0

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

The best way to reach class variable is directly accesing by class name

class Employee:
    counter=0

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

回答 10

在python中,对于所有类型的变量,局部变量,类变量和全局变量,我们都有类似的声明。当您从方法引用全局变量时,python认为您实际上是在从方法本身引用变量,而该变量尚未定义,因此会引发错误。要引用全局变量,我们必须使用globals()[‘variableName’]。

在您的情况下,请分别使用globals()[‘a],globals()[‘b’]和globals()[‘c’]代替a,b和c。

In Python we have similar declaration for all type of variables: local, class, and global variables. When you refer to a global variable from a method, Python thinks that you are actually referring to a variable from the method itself, which is not yet defined and hence it throws an error.

To refer global variable we have to use globals()['variableName'].

in your case use globals()['a], globals()['b'] and globals()['c'] instead of a,b and c respectively.


回答 11

同样的问题困扰着我。使用nonlocalglobal可以解决问题。
但是,使用时需要注意nonlocal,它适用于嵌套函数。但是,在模块级别,它不起作用。在此处查看示例

The same problem bothers me. Using nonlocal and global can solve the problem.
However, attention needed for the usage of nonlocal, it works for nested functions. However, in a module level, it does not work. See examples here.