Python变量范围错误

时间:2008-12-16 03:06:53

标签: 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)行获得UnboundLocalError: 'c' not assigned。正确打印ab的值。这让我感到困惑,原因有两个:

  1. 为什么行(A)会抛出运行时错误,因为后面的语句(B)

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

  3. 我能提出的唯一解释是,本地变量c由作业c+=1创建,该作业优先于“全局”变量{{ 1}}甚至在创建局部变量之前。当然,变量在存在之前“窃取”范围是没有意义的。

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

12 个答案:

答案 0 :(得分:197)

Python会根据您是否从函数中为它们赋值来对函数中的变量进行不同的处理。如果函数包含对变量的任何赋值,则默认情况下将其视为局部变量。因此,当您取消注释该行时,您将尝试在为其分配任何值之前引用局部变量。

如果您希望变量c引用全局c put

global c

作为该函数的第一行。

至于python 3,现在有

nonlocal c

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

答案 1 :(得分:67)

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

现在我们到达c+=1,当然,这相当于c=c+1。当Python扫描该行时,它会显示“aha,有一个名为c的变量,我会将它放入我的本地范围字典中。”然后,当它为赋值右侧的c寻找c的值时,它会找到名为c 的局部变量,它没有任何值,因此抛出错误。< / p>

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

它说它出现问题的原因是因为它在尝试生成代码之前有效地寻找名称,因此在某种意义上并不认为它确实在做那条线。我认为这是一个可用性错误,但通常认为不要认真对待编译器的消息 是一个好习惯。

如果有任何安慰,我可能花了一天时间挖掘并尝试同样的问题,然后才发现Guido写的关于解释一切的词典的内容。

更新,请参阅注释:

它不扫描代码两次,但它会分两个阶段扫描代码,lexing和parsing。

考虑这行代码的解析是如何工作的。词法分析器读取源文本并将其分解为词汇,即语法的“最小组件”。所以当它击中线

c+=1

它将其分解为类似

的内容
SYMBOL(c) OPERATOR(+=) DIGIT(1)

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

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

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

答案 2 :(得分:43)

看一下反汇编可能会澄清发生了什么:

>>> 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行为“获取局部变量的值'a'保存在插槽0中并打印它”,当它检测到此变量仍然未初始化时,会引发异常。

答案 3 :(得分:10)

当您尝试传统的全局变量语义时,Python会有相当有趣的行为。我不记得细节,但你可以很好地阅读在'global'范围内声明的变量的值,但如果你想修改它,你必须使用global关键字。尝试将test()更改为:

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

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

答案 4 :(得分:6)

明确的最好例子是:

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变量范围的完整描述和分析:

答案 5 :(得分:5)

以下是两个可能有用的链接

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

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

答案 6 :(得分:3)

这不是你问题的直接答案,但它是密切相关的,因为它是由增强赋值和函数范围之间的关系引起的另一个问题。

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

所以诀窍是避免对函数参数进行扩充赋值(我尝试只将它用于本地/循环变量)。使用简单的任务,您将避免模棱两可的行为。

答案 7 :(得分:2)

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

我确信您已经知道,'='左侧使用的任何名称都是隐式的局部变量。不止一次,我通过改变对+ =的变量访问而陷入困境,而且它突然变成了另一个变量。

我还想指出它与具体的全球范围没有任何关系。您可以使用嵌套函数获得相同的行为。

答案 8 :(得分:2)

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

使用globalnonlocal个关键字。

nonlocal仅适用于python 3,所以如果您使用python 2而不想让变量成为全局变量,则可以使用可变对象:

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

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

test()

答案 9 :(得分:1)

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

ImageLoader imageLoader = ImageLoader.getInstance();

//download and display image from url
imageLoader.displayImage(url, myImageView);

答案 10 :(得分:0)

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

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

答案 11 :(得分:0)

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