UnboundLocalError 尝试使用一个被(重新)分配的变量(应该是全局的)(即使在第一次使用后)
- 2024-11-15 08:36:00
- admin 原创
- 183
问题描述:
当我尝试此代码时:
a, b, c = (1, 2, 3)
def test():
print(a)
print(b)
print(c)
c += 1
test()
print(c)
我收到以下错误:
UnboundLocalError: local variable 'c' referenced before assignment
或者在某些旧版本中:
UnboundLocalError: 'c' not assigned
如果我注释掉c += 1
,则所有的print
s 都会成功。
我不明白:为什么打印a
和b
工作正常,而打印c
不正常?为什么会c += 1
导致print(c)
失败,即使它出现在代码的后面?
似乎赋值c += 1
会创建一个局部变量c
,该变量优先于全局变量c
。但是变量如何在其存在之前“窃取”范围?为什么c
这里显然是局部的?
另请参阅如何在函数中使用全局变量?有关如何从函数内部重新分配全局变量的问题,以及是否可以修改 Python 中位于外部(封闭)但不在全局范围内的变量?有关从封闭函数(闭包)重新分配的问题。
请参阅为什么不需要使用“global”关键字来访问全局变量?了解 OP预期出现错误但并未收到错误的情况,只需访问不使用global
关键字的全局变量即可。
请参阅Python 中如何“解除”名称的绑定?哪些代码会导致 UnboundLocalError
?了解 OP期望变量是本地变量,但在每种情况下都存在阻止赋值的逻辑错误的情况。
请参阅实际代码中为何会出现“NameError:在封闭范围中赋值之前引用的自由变量‘var’”?以了解由关键字引起的相关问题del
。
解决方案 1:
Python 对待函数中的变量的方式取决于您是从函数内部还是外部为它们赋值。如果在函数内赋值,则默认情况下会将其视为局部变量。因此,当您取消注释该行时,您是在尝试在c
赋值之前引用局部变量。
如果希望变量c
引用c = 3
函数之前分配的全局变量,则输入
global c
作为函数的第一行。
至于 Python 3,现在
nonlocal c
您可以使用它来引用具有变量的最近的封闭函数范围c
。
解决方案 2:
Python 有点奇怪,因为它将所有内容保存在字典中,以适应不同的范围。原始 a、b、c 位于最上层范围,因此位于最上层的字典中。该函数有自己的字典。当您到达print(a)
andprint(b)
语句时,字典中没有该名称的任何内容,因此 Python 会查找列表并在全局字典中找到它们。
现在我们得到c+=1
,它当然等同于c=c+1
。当 Python 扫描该行时,它会说“啊哈,有一个名为 c 的变量,我会把它放入我的本地作用域字典中。”然后,当它为赋值右侧的 c 寻找 c 的值时,它会找到名为 c 的本地变量,该变量还没有值,因此会抛出错误。
上面提到的语句global c
只是告诉解析器,它使用c
来自全局范围的,因此不需要新的。
它之所以说它执行的那行有问题,是因为它在尝试生成代码之前实际上是在寻找名称,因此在某种意义上它认为它还没有真正执行那行。我认为这是一个可用性错误,但通常来说,学会不要太认真对待编译器的消息是一种很好的做法。
如果有任何安慰的话,我花了可能一天的时间挖掘和试验这个问题,然后才找到 Guido 写的关于解释一切的字典的东西。
更新,见评论:
它不会对代码进行两次扫描,但会分两个阶段对代码进行扫描,即词法分析和语法分析。
考虑一下这行代码的解析过程。词法分析器读取源文本并将其分解为词素,即语法的“最小组成部分”。因此,当它遇到以下行时
c+=1
它把它分解成类似
SYMBOL(c) OPERATOR(+=) DIGIT(1)
解析器最终想要将其变成解析树并执行它,但由于这是一个赋值,因此在执行之前,它会在本地字典中查找名称 c,但找不到它,并将其插入字典中,并将其标记为未初始化。在完全编译的语言中,它只会进入符号表并等待解析,但由于它不会有第二次传递的机会,因此词法分析器会做一些额外的工作,以便以后的工作更轻松。只有这样,它才会看到 OPERATOR,看到规则说“如果你有一个运算符 +=,那么左侧必须已经初始化”并说“糟糕!”
这里的重点是它还没有真正开始解析行。这一切都是在为实际解析做准备,所以行计数器还没有前进到下一行。因此当它发出错误信号时,它仍然认为它在上一行。
正如我所说,您可能会认为这是一个可用性错误,但这实际上是一个相当常见的问题。一些编译器对此更诚实,并说“错误发生在第 XXX 行或附近”,但这个编译器却没有。
解决方案 3:
看一下反汇编代码也许能明白发生了什么:
>>> 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 已在函数内赋值,并将其归类为局部变量。局部变量的访问机制与全局变量的访问机制完全不同 - 它们在框架的变量表中被静态分配一个偏移量,这意味着查找是一种快速索引,而不是像全局变量那样更昂贵的字典查找。因此,Python 将该行读print a
为“获取保存在插槽 0 中的局部变量‘a’的值并打印它”,并且当它检测到此变量仍未初始化时,会引发异常。
解决方案 4:
当您尝试传统的全局变量语义时,Python 的行为相当有趣。我不记得细节了,但您可以很好地读取在“全局”范围内声明的变量的值,但如果您想修改它,则必须使用关键字global
。尝试更改test()
为:
def test():
global c
print(a)
print(b)
print(c) # (A)
c+=1 # (B)
此外,您收到此错误的原因是,您还可以在该函数内声明一个与“全局”变量同名的新变量,并且该变量将完全独立。解释器认为您正在尝试在此范围内创建一个新变量,c
并在一次操作中对其进行修改,这在 Python 中是不允许的,因为这个新变量c
尚未初始化。
解决方案 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-mistakes-part-2/
这篇文章提供了 Python 变量作用域的完整描述和分析:
解决方案 6:
概括
Python提前决定了变量的范围。除非使用or (在 3.x 中)关键字明确覆盖,否则变量将根据是否存在会更改名称绑定的操作而被识别为本地变量。这包括普通赋值、增强赋值(如)、各种不太明显的赋值形式(构造、嵌套函数和类、语句……)以及取消绑定(使用)。此类代码的实际执行无关紧要。global
`nonlocal+=
forimport
del`
这也在文档中进行了解释。
讨论
与普遍看法相反, Python在任何意义上都不是“解释型”语言。(现在这种语言已经非常少见了。)Python 的参考实现编译 Python 代码的方式与 Java 或 C# 非常相似:它被翻译成虚拟机的操作码(“字节码”) ,然后进行模拟。其他实现也必须编译代码 - 这样就SyntaxError
可以在不实际运行代码的情况下检测到 s,并实现标准库的“编译服务”部分。
Python 如何确定变量范围
在编译期间(无论是否在参考实现上),Python遵循简单的规则来决定函数中的变量范围:
如果函数包含名称的
global
或nonlocal
声明,则该名称将分别被视为引用全局范围或包含该名称的第一个封闭范围。否则,如果它包含用于更改名称绑定(分配或删除)的任何语法,即使代码在运行时实际上不会更改绑定,该名称也是本地的。
否则,它指的是包含该名称的第一个封闭范围,或者全局范围。
重要的是,范围是在编译时解析的。生成的字节码将直接指示查找位置。例如,在 CPython 3.8 中,有单独的操作码LOAD_CONST
(编译时已知的常量)、LOAD_FAST
(局部变量)、LOAD_DEREF
(nonlocal
通过查找闭包来实现查找,闭包以“单元”对象元组的形式实现)、LOAD_CLOSURE
(在为嵌套函数创建的闭包对象中查找局部变量)和LOAD_GLOBAL
(在全局命名空间或内置命名空间中查找某些内容)。
这些名称没有“默认”值。如果在查找之前尚未分配,则会NameError
出现。具体来说,对于本地查找,UnboundLocalError
会出现;这是的子类型NameError
。
特殊(和非特殊)情况
这里有一些重要的考虑因素,请记住语法规则是在编译时实现的,没有静态分析:
如果全局变量是内置函数等,而不是明确创建的全局变量,则
无关紧要:
def x():
int = int('1') # `int` is local!
(当然,无论如何,隐藏这样的内置名称都是一个坏主意,并且global
没有帮助——就像在函数之外使用相同的代码仍然会导致问题一样。)
即使代码永远无法到达也
没有关系:
y = 1
def x():
return y # local!
if False:
y = 0
如果将分配优化为就地修改(例如扩展列表),则无关紧要 - 从概念上讲,该值仍被分配,并且这在参考实现中的字节码中反映为将名称无用地重新分配给同一
对象:
y = []
def x():
y += [1] # local, even though it would modify `y` in-place with `global`
但是,如果我们改为进行索引/切片分配,那就有关系了。(这会在编译时转换为不同的操作码,进而调用
__setitem__
。)
y = [0]
def x():
print(y) # global now! No error occurs.
y[0] = 1
还有其他形式的赋值,例如
for
循环和import
:
import sys
y = 1
def x():
return y # local!
for y in []:
pass
def z():
print(sys.path) # `sys` is local!
import sys
导致问题的另一种常见方式
import
是尝试将模块名称重用为局部变量,如下所示:
import random
def x():
random = random.choice(['heads', 'tails'])
同样,import
是赋值,因此有一个全局变量random
。但这个全局变量并不特殊;它可以很容易地被本地变量所遮蔽random
。
删除某些内容也会改变名称绑定,例如:
y = 1
def x():
return y # local!
del y
鼓励感兴趣的读者使用参考实现,使用dis
标准库模块检查每个示例。
封闭范围和nonlocal
关键字(在 3.x 中)
对于和关键字,该问题的工作方式相同,但有所改动。(Python 2.x没有。)无论哪种方式,关键字都是从外部范围分配给变量所必需的,但不必仅仅查找它,也不必改变查找到的对象。(再次:列表上的列表会改变列表,但随后还会将名称重新分配给同一列表。)global
`nonlocalnonlocal
+=`
关于全局变量和内置变量的特别说明
如上所示,Python 不会将任何名称视为“在内置作用域内”。相反,内置函数是全局作用域查找使用的后备函数。赋值给这些变量只会更新全局作用域,而不会更新内置作用域。但是,在参考实现中,可以修改内置作用域:它由全局命名空间中名为的变量表示__builtins__
,该变量包含一个模块对象(内置函数是用 C 实现的,但可用作名为的标准库模块builtins
,该模块已预先导入并分配给该全局名称)。奇怪的是,与许多其他内置对象不同,此模块对象的属性可以修改和del
删除。(据我所知,所有这些都应该被视为不可靠的实现细节;但它已经以这种方式工作了相当长一段时间。)
解决方案 7:
以下两个链接可能会有所帮助
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)
解决方案 8:
这不是对您的问题的直接回答,但它密切相关,因为它是由增强分配和函数范围之间的关系引起的另一个陷阱。
在大多数情况下,您倾向于认为增强赋值 ( 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
因此,诀窍是避免对函数参数进行增强赋值(我尝试仅将其用于局部/循环变量)。使用简单赋值,您将不会出现模棱两可的行为。
解决方案 9:
Python 解释器会将函数作为一个完整的单元来读取。我认为它分两步读取,第一步是收集它的闭包(局部变量),第二步是将其转换为字节码。
我确信您已经知道,任何在“=”左侧使用的名称都隐含地是局部变量。我曾多次遇到将变量访问更改为 += 时出错的情况,结果它突然变成了另一个变量。
我还想指出,这实际上与全局范围没有任何关系。使用嵌套函数可以获得相同的行为。
解决方案 10:
c+=1
分配c
,python 假定分配的变量是本地的,但在这种情况下它尚未在本地声明。
使用global
或nonlocal
关键字。
nonlocal
仅适用于 python 3,因此如果您使用的是 python 2 并且不想将变量设为全局变量,则可以使用可变对象:
my_variables = { # a mutable object
'c': 3
}
def test():
my_variables['c'] +=1
test()
解决方案 11:
del
在初始化之后,通常在循环或条件块中,当在变量上使用关键字时,也会出现此问题。
解决方案 12:
在下面这种情况n = num
下,n
是局部变量,num
是全局变量:
num = 10
def test():
# ↓ Local variable
n = num
# ↑ Global variable
print(n)
test()
因此,没有错误:
10
但在下面这种情况num = num
下,num
两边都是局部变量,而num
右边尚未定义:
num = 10
def test():
# ↓ Local variable
num = num
# ↑ Local variable not defined yet
print(num)
test()
因此,出现以下错误:
UnboundLocalError:局部变量‘num’在赋值前被引用
此外,即使num = 10
按照如下所示进行删除:
# num = 10 # Removed
def test():
# ↓ Local variable
num = num
# ↑ Local variable not defined yet
print(num)
test()
下面有同样的错误:
UnboundLocalError:局部变量‘num’在赋值前被引用
因此,为了解决上述错误,请按如下所示global num
输入:num = num
num = 10
def test():
global num # Here
num = num
print(num)
test()
然后上面的错误就解决了,如下图所示:
10
num = 5
或者,像下面这样定义局部变量num = num
:
num = 10
def test():
num = 5 # Here
num = num
print(num)
test()
然后上面的错误就解决了,如下图所示:
5
解决方案 13:
为了修复这个错误,我们可以使用 nonlocal 关键字,它允许您修改非全局的最近封闭范围内的变量。
class S:
def fun1(self):
left_depth = right_depth = 0
def inner():
nonlocal left_depth
nonlocal right_depth
a = 5
left_depth += 1
print(a, left_depth)
inner()
s = S()
s.fun1()
解决方案 14:
访问类变量的最佳方式是直接通过类名访问
class Employee:
counter=0
def __init__(self):
Employee.counter+=1
解决方案 15:
如果您定义一个与方法同名的变量,也会收到此消息。
例如:
def teams():
...
def some_other_method():
teams = teams()
解决方案是将方法重命名teams()
为其他名称,例如get_teams()
。
由于仅在本地使用,Python 消息相当具有误导性!
你最终会得到类似这样的解决方法:
def get_teams():
...
def some_other_method():
teams = get_teams()
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)