问题:首次使用后重新分配时,局部变量上出现UnboundLocalError
以下代码可在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)行得到了注释。的值a
和b
被正确地打印。这使我完全困惑,原因有两个:
为什么由于行(B)的后面的语句而在行(A)抛出运行时错误?
为什么在按预期方式打印变量
a
并产生错误?b
c
我能提出的唯一解释是,局部变量c
是由赋值创建的c+=1
,它c
甚至在创建局部变量之前就优先于“全局”变量。当然,在变量存在之前“窃取”范围是没有意义的。
有人可以解释这种现象吗?
回答 0
Python对函数中的变量的处理方式有所不同,具体取决于您是从函数内部还是外部为其分配值。如果在函数中分配了变量,则默认情况下会将其视为局部变量。因此,当取消注释该行时,您将尝试c
在分配任何值之前引用该局部变量。
如果要让变量c
引用c = 3
该函数之前分配的全局变量,请输入
global c
作为函数的第一行。
至于python 3,现在有
nonlocal c
您可以用来引用最近的包含c
变量的封闭函数范围。
回答 1
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行或其附近的错误”,但事实并非如此。
回答 2
看一下反汇编可以澄清正在发生的事情:
>>> 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’的值,并打印出来”,并且当它检测到该变量仍未初始化时,将引发异常。
回答 3
当您尝试传统的全局变量语义时,Python具有相当有趣的行为。我不记得详细信息,但是您可以很好地读取在“全局”范围内声明的变量的值,但是如果要修改它,则必须使用global
关键字。尝试更改test()
为此:
def test():
global c
print(a)
print(b)
print(c) # (A)
c+=1 # (B)
另外,出现此错误的原因是因为您还可以在该函数内声明一个新变量,其名称与“全局”变量相同,因此它将是完全独立的。解释器认为您正在尝试在此范围内创建一个新变量,c
并在一个操作中对其进行全部修改,这在Python中是不允许的,因为c
未初始化此新变量。
回答 4
清楚说明的最佳示例是:
bar = 42
def foo():
print bar
if False:
bar = 0
在调用时foo()
,尽管我们永远都不会到达line ,但这也会引发问题 ,因此从逻辑上讲,绝对不应创建局部变量。UnboundLocalError
bar=0
神秘之处在于“ Python是一种解释性语言 ”,并且函数的声明foo
被解释为单个语句(即复合语句),它只是笨拙地解释它并创建局部和全局作用域。因此bar
在执行之前会在本地范围内被识别。
有关此类的更多示例,请阅读以下文章:http : //blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/
这篇文章提供了Python变量作用域的完整说明和分析:
回答 5
这里有两个链接可能会有所帮助
链接一描述了错误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
这不是您问题的直接答案,而是紧密相关的,因为这是由扩展分配和函数作用域之间的关系引起的另一个难题。
在大多数情况下,您倾向于认为扩充分配(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
Python解释器将读取一个函数作为一个完整的单元。我认为它是通过两次读取来读取的,一次是收集其闭包(局部变量),另一次是将其转换为字节码。
如您所知,您可能已经知道,在’=’左边使用的任何名称都隐含一个局部变量。我不止一次地通过将变量访问更改为+ =被发现,这突然是一个不同的变量。
我还想指出,这实际上与全局范围无关。嵌套函数具有相同的行为。
回答 8
c+=1
Assigns c
,python假定已分配的变量是局部变量,但在这种情况下,尚未在本地声明。
使用global
或nonlocal
关键字。
nonlocal
仅在python 3中有效,因此,如果您使用的是python 2,并且不想将变量设置为全局变量,则可以使用可变对象:
my_variables = { # a mutable object
'c': 3
}
def test():
my_variables['c'] +=1
test()
回答 9
到达类变量的最佳方法是直接通过类名称访问
class Employee:
counter=0
def __init__(self):
Employee.counter+=1
回答 10
在python中,对于所有类型的变量,局部变量,类变量和全局变量,我们都有类似的声明。当您从方法引用全局变量时,python认为您实际上是在从方法本身引用变量,而该变量尚未定义,因此会引发错误。要引用全局变量,我们必须使用globals()[‘variableName’]。
在您的情况下,请分别使用globals()[‘a],globals()[‘b’]和globals()[‘c’]代替a,b和c。
回答 11
同样的问题困扰着我。使用nonlocal
并global
可以解决问题。
但是,使用时需要注意nonlocal
,它适用于嵌套函数。但是,在模块级别,它不起作用。在此处查看示例。