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


208

以下代码可在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甚至在创建局部变量之前就优先于“全局”变量。当然,在变量存在之前“窃取”范围是没有意义的。

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

Answers:


216

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

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

global c

作为函数的第一行。

至于python 3,现在有

nonlocal c

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


3
谢谢。快速提问。这是否意味着Python在运行程序之前确定每个变量的范围?在运行功能之前?
tba

7
变量范围决定由编译器决定,通常在您首次启动程序时运行一次。但是,请记住,如果程序中有“ eval”或“ exec”语句,则编译器也可能稍后运行。
格雷格·休吉尔

2
好的谢谢。我想“翻译语言”并不像我想的那样暗示。
tba

1
嗯,“ nonlocal”关键字正是我在寻找的东西,似乎Python缺少此关键字。大概此“级联”遍历了使用此关键字导入变量的每个封闭范围?
布伦丹

6
@brainfsck:如果在“查找”和“分配”变量之间进行区分,则最容易理解。如果在当前作用域中找不到名称,查找将退回到更高的作用域。分配总是在本地范围内完成(除非您使用globalnonlocal强制执行全局或非本地分配)
史蒂文

71

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行或其附近的错误”,但事实并非如此。


1
好的,谢谢您的回复;它为我清除了有关python范围的一些知识。但是,我仍然不明白为什么在(A)行而不是(B)行出现错误。Python在运行程序之前是否会创建其可变范围字典?
tba

1
不,它在表达水平上。我将添加到答案中,我认为我无法在评论中添加此内容。
查理·马丁

2
关于实现细节的注意事项:在CPython中,本地范围通常不作为a来处理dict,它在内部只是一个数组(locals()将填充a dict以返回,但对其进行的更改不会创建new locals)。解析阶段是查找到本地的每个分配,并从名称转换为该数组中的位置,并在引用名称时使用该位置。在进入函数时,非参数本地变量会初始化为占位符,并且UnboundLocalError在读取变量且其关联索引仍具有占位符值时发生。
ShadowRanger

44

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

>>> 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'的值,并打印出来”,并且当它检测到该变量仍未初始化时,将引发异常。


10

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

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

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


感谢您的答复,但我认为这不能解释为什么在行(A)抛出错误的原因,我只是在尝试打印变量。程序永远不会进入试图修改未初始化变量的行(B)。
tba

1
Python将在开始运行程序之前读取,解析整个函数并将其转换为内部字节码,因此,“将c转换为局部变量”在文本上显示在值的打印后实际上并不重要。
Vatine

6

清楚说明的最佳示例是:

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变量作用域的完整说明和分析:


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)

4

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

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

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


2

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

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

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


2

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

使用globalnonlocal关键字。

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

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

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

test()

1

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

class Employee:
    counter=0

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

0

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

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


0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.