当重新分配后首次使用时的UnboundLocalError局部变量

2024-04-27 12:42:41 发布

您现在位置:Python中文网/ 问答频道 /正文

以下代码在Python2.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)处会得到一个UnboundLocalError: 'c' not assigned。正确打印ab的值。这让我完全困惑,原因有二:

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

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

我能想到的唯一解释是,一个局部变量c是由赋值c+=1创建的,它在创建局部变量之前就接管了“全局”变量c。当然,变量在存在之前“窃取”作用域是没有意义的。

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


Tags: 代码testdef错误not原因语句全局
3条回答

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

现在我们来讨论c+=1,这当然相当于c=c+1。当Python扫描那一行时,它会说“aha,有一个名为c的变量,我会把它放到我的本地作用域字典中。”然后当它在赋值的右边为c寻找一个c的值时,它会找到它的名为c的本地变量,它还没有值,因此抛出错误。

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

它之所以说这行代码有问题,是因为它在尝试生成代码之前有效地查找了名称,所以在某种意义上说它还没有真正执行这行代码。我认为这是一个可用性缺陷,但一般来说,只要学会不要把编译器的消息也当真就好了。

如果有什么安慰的话,我可能花了一天的时间来研究和试验这个问题,直到我发现Guido写了一些关于解释一切的字典的东西。

更新,请参阅评论:

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

考虑一下这一行代码的解析是如何工作的。lexer读取源文本并将其分解为lexems,这是语法的“最小组件”。所以当它触线的时候

c+=1

它把它分解成

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

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

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

正如我所说,你可以说这是一个可用性缺陷,但实际上这是一个相当普遍的事情。有些编译器对此比较诚实,说“第XXX行或第XXX行附近有错误”,但这一行没有

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

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

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

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

global c

作为函数的第一行。

至于python 3,现在

nonlocal c

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

相关问题 更多 >