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

Python 首次使用后重新分配局部变量时出现UnboundLocalError,python,variables,scope,Python,Variables,Scope,以下代码在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)行时,我在第(A)行得到一个未绑定的本地错误:“c”未分配。a和b的值打印正确。这让我完全困惑,原因有两个: 为什么第(a)行出现运行时错误,因为第(B)行后面有一

以下代码在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)行时,我在第(A)行得到一个
未绑定的本地错误:“c”未分配。
a
b
的值打印正确。这让我完全困惑,原因有两个:

  • 为什么第(a)行出现运行时错误,因为第(B)行后面有一条语句

  • 为什么变量
    a
    b
    按预期打印,而
    c
    会引发错误

  • 我能给出的唯一解释是,局部变量
    c
    是通过赋值
    c+=1
    创建的,即使在创建局部变量之前,它也先于“全局”变量
    c
    。当然,变量在存在之前“窃取”作用域是没有意义的


    有人能解释一下这种行为吗?

    Python根据您是从函数内部还是外部给变量赋值,对函数中的变量进行不同的处理。如果在函数中指定了变量,则默认情况下将其视为局部变量。因此,当您取消注释该行时,您正试图在为其指定任何值之前引用局部变量
    c

    如果希望变量
    c
    引用函数前分配的全局
    c=3
    ,请将

    global c
    
    作为函数的第一行

    至于Python3,现在有

    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的值时,它会找到名为c的局部变量,该变量还没有值,因此抛出错误

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

    它之所以说这行代码有问题,是因为它在尝试生成代码之前有效地查找了名称,所以从某种意义上说,它认为它还没有真正做到这一行。我认为这是一个可用性缺陷,但一般来说,学习不要把编译器的消息看得太重是一个很好的实践

    如果有什么安慰的话,我可能花了一天的时间挖掘和试验同一个问题,然后才发现Guido写的关于解释一切的词典的东西

    更新,见评论: 它不会扫描代码两次,但会分两个阶段扫描代码,词法分析和解析

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

    c+=1
    
    它把它分解成这样的东西

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

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


    正如我所说,你可能会说这是一个可用性缺陷,但实际上这是一个相当普遍的问题。有些编译器对它更诚实,说“XXX行上或周围有错误”,但这个编译器不这么认为。

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

    我相信您已经知道,在“=”左边使用的任何名称都隐含着局部变量。我不止一次被发现将变量访问权限更改为+=而它突然变成了另一个变量

    我还想指出,它实际上与全局范围无关。你也有同样的行为
    >>> 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 = a + b
    
    a.__iadd__(b)
    
    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
    
    >>> 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
    
    class Employee:
        counter=0
    
        def __init__(self):
            Employee.counter+=1
    
    my_variables = { # a mutable object
        'c': 3
    }
    
    def test():
        my_variables['c'] +=1
    
    test()