UnboundLocalError 尝试使用(重新)分配的变量(应该是全局变量)(即使在首次使用后)

UnboundLocalError trying to use a variable (supposed to be global) that is (re)assigned (even after first use)

提问人:tba 提问时间:12/16/2008 最后编辑:Karl Knechteltba 更新时间:3/1/2023 访问量:103293

问:

当我尝试此代码时:

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 += 1print

我不明白:为什么不打印和工作?为什么导致失败,即使它出现在代码的后面?abcc += 1print(c)

似乎赋值创建了一个局部变量,该变量优先于全局变量。但是,变量如何在范围存在之前“窃取”范围呢?为什么这里显然是本地的?c += 1ccc


另请参阅在函数中使用全局变量,了解关于如何从函数内部重新分配全局变量的问题,以及是否可以在 python 中修改外部(封闭)但不是全局作用域中的变量? 用于从封闭函数(闭包)重新分配。

请参阅为什么访问全局变量不需要“global”关键字? 对于 OP 预期错误但没有收到错误的情况,只需访问没有 global 关键字的全局。

请参阅如何在 Python 中“取消绑定”名称?什么代码会导致“UnboundLocalError”?对于 OP 期望变量是局部变量,但存在逻辑错误,导致在每种情况下都无法赋值的情况。

Python 作用域 全局 局部变量 阴影

评论


答:

306赞 recursive 12/16/2008 #1

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

如果希望变量引用函数之前分配的全局变量,请将cc = 3

global c

作为函数的第一行。

至于python 3,现在有

nonlocal c

可用于引用具有变量的最近封闭函数作用域。c

评论

8赞 tba 12/16/2008
谢谢。快速提问。这是否意味着 Python 在运行程序之前决定每个变量的范围?在运行函数之前?
14赞 Greg Hewgill 12/16/2008
变量作用域决策由编译器做出,编译器通常在您首次启动程序时运行一次。但是,值得记住的是,如果程序中有“eval”或“exec”语句,编译器也可能稍后运行。
3赞 tba 12/16/2008
好的,谢谢。我想“解释语言”并不像我想象的那么重要。
1赞 Brendan 11/18/2009
啊,那个“非本地”关键字正是我想要的,似乎 Python 缺少这个。据推测,这“级联”通过使用此关键字导入变量的每个封闭范围?
8赞 Steven 9/13/2011
@brainfsck:如果你区分“查找”和“分配”变量,这是最容易理解的。如果在当前作用域中找不到名称,则查找将回退到更高的作用域。分配始终在本地作用域中完成(除非您使用或强制执行全局或非本地分配)globalnonlocal
12赞 Mongoose 12/16/2008 #2

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

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

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

评论

0赞 tba 12/16/2008
感谢您的回复,但我认为这并不能解释为什么在(A)行抛出错误,我只是尝试打印一个变量。程序永远不会到达 (B) 行,它试图修改未初始化的变量。
1赞 Vatine 12/16/2008
Python 会在开始运行程序之前读取、解析整个函数并将其转换为内部字节码,因此在打印值后“将 c 转换为局部变量”这一事实在文本上发生并不重要。
1赞 Mark Seagoe 7/21/2022
Python 允许您访问本地范围内的全局变量以进行读取,但不能用于写入。这个答案有一个很好的解决方法,在下面的评论中进行了解释...... +=1。
93赞 Charlie Martin 12/16/2008 #3

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

现在我们得到 ,当然,它等价于 。当 Python 扫描那一行时,它会说“啊哈,有一个名为 c 的变量,我会把它放到我的本地范围字典中。然后,当它在赋值右侧的 c 中查找 c 的值时,它会找到名为 c 的局部变量,该变量还没有值,因此引发了错误。c+=1c=c+1

上面提到的语句只是告诉解析器它使用 from 全局范围,因此不需要新的。global cc

它之所以说它所做的行存在问题,是因为它在尝试生成代码之前有效地寻找名称,因此从某种意义上说,它认为它还没有真正执行该行。我认为这是一个可用性错误,但通常学会不要认真地对待编译器的消息是一种很好的做法。

如果有什么安慰的话,我大概花了一天时间挖掘和试验同样的问题,然后我才找到圭多写的关于解释一切的词典的东西。

更新,见评论:

它不会扫描代码两次,但会分两个阶段扫描代码,即词法分析和解析。

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

c+=1

它把它分解成类似的东西

SYMBOL(c) OPERATOR(+=) DIGIT(1)

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

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

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

评论

7赞 ShadowRanger 3/26/2016
关于实现细节的说明:在 CPython 中,本地范围通常不会作为 处理,它在内部只是一个数组( 将填充 a 以返回,但对它的更改不会创建新的 )。分析阶段是查找每个对本地值的赋值,并在该数组中从名称转换为位置,并在引用名称时使用该位置。在进入函数时,非参数局部变量被初始化为占位符,当读取变量并且其关联索引仍具有占位符值时,就会发生 s。dictlocals()dictlocalsUnboundLocalError
0赞 Karl Knechtel 2/7/2023
Python 3.x 不会在字典中保留局部变量。的结果是动态计算的。这就是首先调用错误的原因:局部变量的存在在于,它在编译函数时提前保留,但尚未绑定(赋值)。这与向全局命名空间(实际上是一个字典)添加某些内容的工作方式根本不同,因此将问题报告为泛型是没有意义的。locals()UnboundLocalErrorNameError
0赞 Charlie Martin 2/14/2023
这正是我所说的,模出 locals() 如何呈现的实现细节。
0赞 Charlie Martin 2/14/2023
哦,这个答案实际上是 15 岁。
0赞 NameError 10/26/2023
“然后,当它在赋值的右侧为 c 寻找 c 的值时,它会找到名为 c 的局部变量,该变量还没有值,因此抛出错误” 但是,如果我改为将行更改为 ,为什么错误仍然存在?蟒蛇-3.10c += 1c = 100
2赞 James Hopkin 12/16/2008 #4

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

我相信您已经知道,“=”左侧使用的任何名称都隐含着局部变量。我不止一次因将变量访问更改为 += 而陷入困境,它突然变成了一个不同的变量。

我还想指出,这与全球范围无关。使用嵌套函数,您将获得相同的行为。

53赞 Brian 12/16/2008 #5

看一下拆解可能会弄清楚发生了什么:

>>> 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_FASTLOAD_GLOBALprint a

2赞 alsuren 1/24/2009 #6

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

在大多数情况下,您倾向于认为增强赋值 () 完全等同于简单赋值 ()。不过,在一个极端情况下,可能会遇到一些麻烦。让我解释一下:a += ba = a + b

Python 的简单赋值的工作方式意味着,如果被传递到函数中(比如;请注意,Python 始终是按引用传递的),那么不会修改传入的内容。相反,它只会修改指向 的本地指针。afunc(a)a = a + baa

但是,如果您使用 ,那么它有时实现为:a += b

a = a + b

或者有时(如果该方法存在)为:

a.__iadd__(b)

在第一种情况下(只要没有声明为全局),在局部范围之外没有副作用,因为分配给只是一个指针更新。aa

在第二种情况下,实际上会修改自己,所以所有的引用都将指向修改后的版本。以下代码演示了这一点:aa

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

所以诀窍是避免对函数参数进行增强赋值(我尝试只将其用于局部/循环变量)。使用简单的分配,您将免受模棱两可的行为的影响。

5赞 mcdon 11/17/2009 #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

链接 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)
8赞 Sahil kalra 6/4/2014 #8

最好的例子是:

bar = 42
def foo():
    print bar
    if False:
        bar = 0

调用时,这也引发了虽然我们永远无法到达行,但逻辑上永远不应该创建局部变量。foo()UnboundLocalErrorbar=0

谜团在于“Python 是一种解释型语言”,函数的声明被解释为单个语句(即复合语句),它只是愚蠢地解释它并创建局部和全局范围。因此在执行之前在本地范围内被识别。foobar

有关更多此类示例,请阅读这篇文章: http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

这篇文章提供了对变量的 Python 范围的完整描述和分析:

评论

1赞 Karl Knechtel 9/9/2022
Python 并不比 Java 或 C# 更“解释”,事实上,在此代码中将其视为局部变量的决定需要预先编译步骤。bar
0赞 Harun ERGUL 12/8/2015 #9

获取类变量的最佳方法是直接按类名访问

class Employee:
    counter=0

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

评论

1赞 Karl Knechtel 2/7/2023
这与提出的问题无关。
2赞 Colegram 11/4/2016 #10

c+=1assigns ,Python 假定分配的变量是本地变量,但在本例中,它尚未在本地声明。c

使用 or 关键字。globalnonlocal

nonlocal仅适用于 Python 3,因此,如果您使用的是 Python 2 并且不想让变量全局化,则可以使用可变对象:

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

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

test()
-1赞 JGFMK 4/18/2022 #11

如果定义与方法同名的变量,也可以收到此消息。

例如:

def teams():
    ...

def some_other_method():
    teams = teams()

解决方案是将方法重命名为其他名称,例如 .teams()get_teams()

由于它仅在本地使用,因此 Python 消息具有相当大的误导性!

你最终会得到这样的东西来绕过它:

def get_teams():
    ...

def some_other_method():
    teams = get_teams()
0赞 izilotti 6/15/2022 #12

当在初始化后(通常在循环或条件块中)在变量上使用关键字时,也会发生此问题。del

5赞 Karl Knechtel 9/9/2022 #13

总结

Python 提前决定变量的作用域。除非使用 globalnonlocal(在 3.x 中)关键字显式覆盖,否则变量将根据是否存在任何会更改名称绑定的操作被识别为局部变量。这包括普通赋值、增强赋值,如、各种不太明显的赋值形式(构造、嵌套函数和类、语句等)以及取消绑定(使用 )。此类代码的实际执行是无关紧要的。+=forimportdel

文档中对此也有解释。

讨论

与流行的看法相反,Python 在任何有意义的意义上都不是一种“解释型”语言。(这些现在已经非常罕见了。Python 的参考实现以与 Java 或 C# 大致相同的方式编译 Python 代码:将其转换为虚拟机的操作码(“字节码”),然后对其进行模拟。其他实现也必须编译代码 - 以便在不实际运行代码的情况下可以检测到 s,并且为了实现标准库的“编译服务”部分。SyntaxError

Python 如何确定变量范围

在编译过程中(无论是否在参考实现上),Python 都遵循简单的规则来决定函数中的变量范围:

  • 如果函数包含名称的全局声明或非本地声明,则该名称将分别被视为引用全局作用域或包含该名称的第一个封闭作用域。

  • 否则,如果它包含任何用于更改名称绑定(赋值或删除)的语法,即使代码在运行时实际上不会更改绑定,该名称也是本地的。

  • 否则,它引用包含名称的第一个封闭作用域,或者引用全局作用域。

重要的是,范围在编译时被解析。生成的字节码将直接指示要查找的位置。例如,在 CPython 3.8 中,有单独的操作码(编译时已知的常量)、(局部变量)、(通过查看闭包来实现查找,该闭包作为“单元”对象的元组实现)、(在为嵌套函数创建的闭包对象中查找局部变量)和(在全局命名空间或内置命名空间中查找某些内容)。LOAD_CONSTLOAD_FASTLOAD_DEREFnonlocalLOAD_CLOSURELOAD_GLOBAL

这些名称没有“默认值”。如果在查找之前尚未分配它们,则会发生 a。具体来说,对于本地查找,发生;这是 的子类型。NameErrorUnboundLocalErrorNameError

特殊(和非特殊)情况

这里有一些重要的注意事项,请记住,语法规则是在编译时实现的,没有静态分析

  • 全局变量是否是内置函数等,而不是显式创建的全局变量并不重要:(当然,无论如何,像这样隐藏内置名称是一个坏主意,并且无济于事 - 就像在函数之外使用相同的代码仍然会导致问题一样。
    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:forimport
    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'])
    
    importrandomrandom
  • 删除也是更改名称绑定,例如:
    y = 1
    def x():
        return y # local!
        del y
    

鼓励有兴趣的读者使用参考实现,使用标准库模块检查这些示例中的每一个。dis

封闭范围和关键字(在 3.x 中)nonlocal

问题比适用于两者和关键字。(Python 2.x 没有非本地。无论哪种方式,关键字对于从外部作用域分配给变量都是必需的,但不仅仅是查找它改变查找对象所必需的。(同样:在列表中会更改列表,但随后还会将名称重新分配给同一列表。globalnonlocal+=

关于全局变量和内置函数的特别说明

如上所述,Python 不会将任何名称视为“在内置范围内”。相反,内置是全局范围查找使用的回退。分配给这些变量只会更新全局作用域,而不会更新内置作用域。但是,在参考实现中,可以修改内置范围:它由全局命名空间中一个名为 的变量表示,该变量包含一个模块对象(内置模块是用 C 实现的,但作为名为 的标准库模块提供,该模块已预先导入并分配给该全局名称)。奇怪的是,与许多其他内置对象不同,这个模块对象可以修改其属性和 d.(据我所知,所有这些都被认为是一个不可靠的实现细节;但它已经以这种方式工作了很长一段时间。__builtins__builtinsdel

0赞 Super Kai - Kazuya Ito 1/8/2023 #14

在下面的这种情况中,是一个局部变量,也是一个全局变量:n = numnnum

num = 10

def test():
  # ↓ Local variable
    n = num
       # ↑ Global variable
    print(n)
  
test()

所以,没有错误:

10

但是在下面的这个例子中,两边都是局部变量,右边还没有定义:num = numnumnum

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 numnum = num

num = 10

def test():
    global num # Here
    num = num 
    print(num)
  
test()

然后,解决上述错误,如下图所示:

10

或者,定义 before 的局部变量,如下所示:num = 5num = num

num = 10

def test():
    num = 5 # Here
    num = num
    print(num)
  
test()

然后,解决上述错误,如下图所示:

5