python变量范围错误

Python variable scope error

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

1
2
3
4
5
6
7
8
9
10
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)行得到一个UnboundLocalError: 'c' not assigned。正确打印ab的值。这让我完全困惑,原因有两个:

  • 为什么第(a)行由于第(b)行上的稍后语句而引发运行时错误?

  • 为什么变量ab按预期打印,而c会产生错误?

  • 我唯一能想到的解释是,局部变量c是由赋值c+=1创建的,它比"全局"变量c具有先例,甚至在创建局部变量之前。当然,变量在存在之前"窃取"作用域是没有意义的。

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


    python对函数中的变量的处理方式不同,这取决于是否从函数中为变量赋值。如果函数包含对变量的任何赋值,则默认情况下它被视为局部变量。因此,当取消对行的注释时,您试图在为局部变量赋值之前引用它。

    如果希望变量c引用全局cput

    1
    global c

    作为函数的第一行。

    至于python 3,现在有了

    1
    nonlocal c

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


    Python有点奇怪,因为它将所有内容都保存在字典中,用于各种范围。原来的A,B,C在最上面的范围内,所以在最上面的字典里。函数有自己的字典。当您到达print(a)print(b)语句时,字典中没有该名称,所以python查找列表并在全局字典中找到它们。

    现在我们到了c+=1,这当然相当于c=c+1。当python扫描这一行时,它会说"啊哈,有一个名为c的变量,我会把它放在本地作用域字典中。"然后当它在赋值的右边为c寻找一个值时,它会找到它的名为c的本地变量,这个变量还没有值,所以抛出了错误。

    上面提到的声明global c只是告诉解析器它从全局范围使用c,因此不需要新的声明。

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

    如果有什么安慰的话,我可能花了一天的时间来研究和试验同一个问题,然后才发现吉多写了一些解释一切的字典。

    更新,请参见注释:

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

    考虑这一行代码的解析方式。lexer读取源文本并将其分解为lexems,这是语法的"最小组件"。所以当它碰到线的时候

    1
    c+=1

    它把它分解成

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

    解析器最终希望把它变成一个解析树并执行它,但是由于它是一个赋值,所以在它执行之前,它会在本地字典中查找名称c,看不到它,并将其插入字典,将其标记为未初始化。在完全编译的语言中,它只需进入符号表并等待解析,但是由于它没有第二次传递的奢侈,lexer做了一些额外的工作,以使以后的生活更轻松。只有这样,它才能看到运算符,看到规则说"如果有运算符+=左手边必须已初始化",然后说"哎呀!"

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

    正如我所说,你可以说这是一个可用性缺陷,但实际上这是一个相当常见的问题。有些编译器对此更诚实,并说"行上或行附近的错误",但这一个没有。


    看一看拆卸可能会弄清楚发生了什么:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    >>> 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"的值并打印它",当它检测到该变量仍然未初始化时,会引发异常。


    当您尝试传统的全局变量语义时,python有相当有趣的行为。我不记得详细信息,但是您可以读取"global"范围中声明的变量的值,但如果您想修改它,则必须使用global关键字。尝试将test()更改为:

    1
    2
    3
    4
    5
    6
    def test():
        global c
        print(a)
        print(b)
        print(c)    # (A)
        c+=1        # (B)

    另外,之所以出现此错误,是因为您还可以在该函数中声明一个与"全局"变量同名的新变量,并且该变量将完全独立。解释器认为您正试图在此范围内生成一个名为c的新变量,并在一个操作中对其进行全部修改,这在python中是不允许的,因为这个新的c没有初始化。


    最好的例子是:

    1
    2
    3
    4
    5
    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-errors-part-2/

    本文提供了对变量python作用域的完整描述和分析:


    这里有两个链接可以帮助

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

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

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    >>> 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)


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

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

    python的简单赋值方式意味着,如果a被传递到一个函数(如func(a),注意python总是通过引用传递的),那么a = a + b将不会修改传入的a。相反,它只修改指向a的本地指针。

    但是,如果您使用a += b,那么它有时实现为:

    1
    a = a + b

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

    1
    a.__iadd__(b)

    在第一种情况下(只要a未声明为global),本地范围之外没有副作用,因为对a的分配只是一个指针更新。

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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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

    所以技巧是避免对函数参数进行增广赋值(我尝试只对局部/循环变量使用它)。使用简单的作业,你就不会有模棱两可的行为。


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

    使用globalnonlocal关键字。

    nonlocal只在python3中工作,因此如果您使用的是python2,并且不想使变量成为全局变量,则可以使用可变对象:

    1
    2
    3
    4
    5
    6
    7
    8
    my_variables = { # a mutable object
        'c': 3
    }

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

    test()

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

    正如我确信您已经知道的,在"="左边使用的任何名称都是隐式局部变量。不止一次,我被一个变量的访问权变成了一个+=而它突然变成了一个不同的变量。

    我还想指出,具体来说,这与全球范围无关。使用嵌套函数可以获得相同的行为。


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

    1
    2
    3
    4
    5
    class Employee:
        counter=0

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

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

    在您的情况下,分别使用globals()['a]、globals()['b']和globals()['c']而不是a、b和c。