提问人:tba 提问时间:12/16/2008 最后编辑:Karl Knechteltba 更新时间:3/1/2023 访问量:103293
UnboundLocalError 尝试使用(重新)分配的变量(应该是全局变量)(即使在首次使用后)
UnboundLocalError trying to use a variable (supposed to be global) that is (re)assigned (even after first use)
问:
当我尝试此代码时:
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
在较新版本的 Python 中,或者
UnboundLocalError: 'c' not assigned
在某些旧版本中。
如果我注释掉,两个 s 都是成功的。c += 1
print
我不明白:为什么不打印和工作?为什么导致失败,即使它出现在代码的后面?a
b
c
c += 1
print(c)
似乎赋值创建了一个局部变量,该变量优先于全局变量。但是,变量如何在范围存在之前“窃取”范围呢?为什么这里显然是本地的?c += 1
c
c
c
另请参阅在函数中使用全局变量,了解关于如何从函数内部重新分配全局变量的问题,以及是否可以在 python 中修改外部(封闭)但不是全局作用域中的变量? 用于从封闭函数(闭包)重新分配。
请参阅为什么访问全局变量不需要“global”关键字? 对于 OP 预期错误但没有收到错误的情况,只需访问没有 global
关键字的全局。
请参阅如何在 Python 中“取消绑定”名称?什么代码会导致“UnboundLocalError”?对于 OP 期望变量是局部变量,但存在逻辑错误,导致在每种情况下都无法赋值的情况。
答:
Python 以不同的方式处理函数中的变量,具体取决于您是从函数内部还是外部为变量赋值。如果在函数中赋值了变量,则默认情况下将其视为局部变量。因此,当您取消注释该行时,您尝试在为其分配任何值之前引用局部变量。c
如果希望变量引用函数之前分配的全局变量,请将c
c = 3
global c
作为函数的第一行。
至于python 3,现在有
nonlocal c
可用于引用具有变量的最近封闭函数作用域。c
评论
global
nonlocal
当您尝试传统的全局变量语义时,Python 具有相当有趣的行为。我不记得细节了,但你可以很好地读取在“全局”范围内声明的变量的值,但如果你想修改它,你必须使用关键字。尝试更改为以下内容:global
test()
def test():
global c
print(a)
print(b)
print(c) # (A)
c+=1 # (B)
此外,您收到此错误的原因是,您还可以在该函数中声明一个与“全局”变量同名的新变量,并且它将是完全独立的。解释器认为您正在尝试在此范围内调用一个新变量,并在一个操作中对其进行全部修改,这在 Python 中是不允许的,因为这个新变量未初始化。c
c
评论
Python 有点奇怪,因为它将各种范围的所有内容都保存在字典中。原来的 a、b、c 在最上面的范围内,所以在最上面的字典里。该函数有自己的字典。当您到达 and 语句时,字典中没有该名称的任何内容,因此 Python 查找列表并在全局字典中找到它们。print(a)
print(b)
现在我们得到 ,当然,它等价于 。当 Python 扫描那一行时,它会说“啊哈,有一个名为 c 的变量,我会把它放到我的本地范围字典中。然后,当它在赋值右侧的 c 中查找 c 的值时,它会找到名为 c 的局部变量,该变量还没有值,因此引发了错误。c+=1
c=c+1
上面提到的语句只是告诉解析器它使用 from 全局范围,因此不需要新的。global c
c
它之所以说它所做的行存在问题,是因为它在尝试生成代码之前有效地寻找名称,因此从某种意义上说,它认为它还没有真正执行该行。我认为这是一个可用性错误,但通常学会不要太认真地对待编译器的消息是一种很好的做法。
如果有什么安慰的话,我大概花了一天时间挖掘和试验同样的问题,然后我才找到圭多写的关于解释一切的词典的东西。
更新,见评论:
它不会扫描代码两次,但会分两个阶段扫描代码,即词法分析和解析。
考虑这行代码的解析是如何工作的。词法分析器读取源文本并将其分解为词素,即语法的“最小组成部分”。所以当它上线时
c+=1
它把它分解成类似的东西
SYMBOL(c) OPERATOR(+=) DIGIT(1)
解析器最终希望将其制作成解析树并执行它,但由于它是一个赋值,因此在它这样做之前,它会在本地字典中查找名称 c,看不到它,并将其插入字典中,将其标记为未初始化。在完全编译的语言中,它只会进入符号表并等待解析,但由于它没有第二次传递的奢侈,因此词法分析器会做一些额外的工作,以使以后的生活更轻松。只是,然后它看到运算符,看到规则说“如果你有一个运算符 += 左手边一定已经初始化了”,然后说“哎呀!
这里的重点是它还没有真正开始对行进行解析。这一切都是在为实际解析做准备时发生的,因此行计数器尚未前进到下一行。因此,当它发出错误信号时,它仍然认为它在前一行。
正如我所说,你可以说这是一个可用性错误,但它实际上是一个相当普遍的事情。一些编译器对此更诚实,并说“XXX行或周围有错误”,但这个编译器没有。
评论
dict
locals()
dict
locals
UnboundLocalError
locals()
UnboundLocalError
NameError
c += 1
c = 100
Python 解释器会将函数作为一个完整的单元来读取。我认为它分两次读取它,一次是收集它的闭包(局部变量),另一次是将其转换为字节码。
我相信您已经知道,“=”左侧使用的任何名称都隐含着局部变量。我不止一次因将变量访问更改为 += 而陷入困境,它突然变成了一个不同的变量。
我还想指出,这与全球范围无关。使用嵌套函数,您将获得相同的行为。
看一下拆解可能会弄清楚发生了什么:
>>> 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 的字节码是 ,而 b 的字节码是 。这是因为编译器已识别出 a 在函数中分配给,并将其分类为局部变量。局部变量的访问机制与全局变量根本不同 - 它们在帧的变量表中被静态分配一个偏移量,这意味着查找是一个快速索引,而不是像全局变量那样更昂贵的字典查找。因此,Python 将该行读取为“获取插槽 0 中保存的局部变量 'a' 的值,并打印它”,当它检测到此变量仍未初始化时,会引发异常。LOAD_FAST
LOAD_GLOBAL
print a
这不是对您的问题的直接回答,但它密切相关,因为它是由增强赋值和函数范围之间的关系引起的另一个陷阱。
在大多数情况下,您倾向于认为增强赋值 () 完全等同于简单赋值 ()。不过,在一个极端情况下,可能会遇到一些麻烦。让我解释一下:a += b
a = a + b
Python 的简单赋值的工作方式意味着,如果被传递到函数中(比如;请注意,Python 始终是按引用传递的),那么不会修改传入的内容。相反,它只会修改指向 的本地指针。a
func(a)
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
所以诀窍是避免对函数参数进行增强赋值(我尝试只将其用于局部/循环变量)。使用简单的分配,您将免受模棱两可的行为的影响。
这里有两个链接可能会有所帮助
链接 1 描述错误 UnboundLocalError。链接 2 可以帮助重写测试函数。根据链接二,原来的问题可以改写为:
>>> 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)
最好的例子是:
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 范围的完整描述和分析:
评论
bar
获取类变量的最佳方法是直接按类名访问
class Employee:
counter=0
def __init__(self):
Employee.counter+=1
评论
c+=1
assigns ,Python 假定分配的变量是本地变量,但在本例中,它尚未在本地声明。c
使用 or 关键字。global
nonlocal
nonlocal
仅适用于 Python 3,因此,如果您使用的是 Python 2 并且不想让变量全局化,则可以使用可变对象:
my_variables = { # a mutable object
'c': 3
}
def test():
my_variables['c'] +=1
test()
如果定义与方法同名的变量,也可以收到此消息。
例如:
def teams():
...
def some_other_method():
teams = teams()
解决方案是将方法重命名为其他名称,例如 .teams()
get_teams()
由于它仅在本地使用,因此 Python 消息具有相当大的误导性!
你最终会得到这样的东西来绕过它:
def get_teams():
...
def some_other_method():
teams = get_teams()
当在初始化后(通常在循环或条件块中)在变量上使用关键字时,也会发生此问题。del
总结
Python 提前决定变量的作用域。除非使用 global
或 nonlocal
(在 3.x 中)关键字显式覆盖,否则变量将根据是否存在任何会更改名称绑定的操作被识别为局部变量。这包括普通赋值、增强赋值,如、各种不太明显的赋值形式(构造、嵌套函数和类、语句等)以及取消绑定(使用 )。此类代码的实际执行是无关紧要的。+=
for
import
del
文档中对此也有解释。
讨论
与流行的看法相反,Python 在任何有意义的意义上都不是一种“解释型”语言。(这些现在已经非常罕见了。Python 的参考实现以与 Java 或 C# 大致相同的方式编译 Python 代码:将其转换为虚拟机的操作码(“字节码”),然后对其进行模拟。其他实现也必须编译代码 - 以便在不实际运行代码的情况下可以检测到 s,并且为了实现标准库的“编译服务”部分。SyntaxError
Python 如何确定变量范围
在编译过程中(无论是否在参考实现上),Python 都遵循简单的规则来决定函数中的变量范围:
否则,如果它包含任何用于更改名称绑定(赋值或删除)的语法,即使代码在运行时实际上不会更改绑定,该名称也是本地的。
否则,它引用包含名称的第一个封闭作用域,或者引用全局作用域。
重要的是,范围在编译时被解析。生成的字节码将直接指示要查找的位置。例如,在 CPython 3.8 中,有单独的操作码(编译时已知的常量)、(局部变量)、(通过查看闭包来实现查找,该闭包作为“单元”对象的元组实现)、(在为嵌套函数创建的闭包对象中查找局部变量)和(在全局命名空间或内置命名空间中查找某些内容)。LOAD_CONST
LOAD_FAST
LOAD_DEREF
nonlocal
LOAD_CLOSURE
LOAD_GLOBAL
这些名称没有“默认值”。如果在查找之前尚未分配它们,则会发生 a。具体来说,对于本地查找,发生;这是 的子类型。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
- 还有其他形式的转让,例如 循环和 s:
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
封闭范围和关键字(在 3.x 中)nonlocal
问题比照适用于两者和关键字。(Python 2.x 没有非本地
。无论哪种方式,关键字对于从外部作用域分配给变量都是必需的,但不仅仅是查找它或改变查找对象所必需的。(同样:在列表中会更改列表,但随后还会将名称重新分配给同一列表。global
nonlocal
+=
关于全局变量和内置函数的特别说明
如上所述,Python 不会将任何名称视为“在内置范围内”。相反,内置是全局范围查找使用的回退。分配给这些变量只会更新全局作用域,而不会更新内置作用域。但是,在参考实现中,可以修改内置范围:它由全局命名空间中一个名为 的变量表示,该变量包含一个模块对象(内置模块是用 C 实现的,但作为名为 的标准库模块提供,该模块已预先导入并分配给该全局名称)。奇怪的是,与许多其他内置对象不同,这个模块对象可以修改其属性和 d.(据我所知,所有这些都被认为是一个不可靠的实现细节;但它已经以这种方式工作了很长一段时间。__builtins__
builtins
del
在下面的这种情况中,是一个局部变量,也是一个全局变量: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
或者,定义 before 的局部变量,如下所示:num = 5
num = num
num = 10
def test():
num = 5 # Here
num = num
print(num)
test()
然后,解决上述错误,如下图所示:
5
评论