【问题标题】:UnboundLocalError on local variable when reassigned after first use首次使用后重新分配时局部变量上的 UnboundLocalError
【发布时间】:2021-11-29 14:01:28
【问题描述】:

以下代码在 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) 行时,我会在 (A) 行得到一个UnboundLocalError: 'c' not assignedab 的值打印正确。这让我完全困惑有两个原因:

  1. 为什么在 (A) 行会因为后面的语句在 (B) 行上抛出运行时错误?

  2. 为什么变量ab按预期打印,而c引发错误?

我能想到的唯一解释是 local 变量 c 是由赋值 c+=1 创建的,它甚至在“全局”变量 c 之前创建局部变量。当然,在变量存在之前“窃取”作用域是没有意义的。

有人可以解释一下这种行为吗?

【问题讨论】:

标签: python variables scope


【解决方案1】:

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

如果想让变量c引用函数前赋值的全局c = 3,放

global c

作为函数的第一行。

至于python 3,现在有

nonlocal c

您可以使用它来引用最近的具有c 变量的封闭函数范围。

【讨论】:

  • 谢谢。快速提问。这是否意味着 Python 在运行程序之前决定了每个变量的范围?在运行函数之前?
  • 变量作用域由编译器决定,通常在你第一次启动程序时运行一次。但是值得记住的是,如果您的程序中有“eval”或“exec”语句,编译器也可能稍后运行。
  • 好的,谢谢。我想“解释性语言”的含义并不像我想象的那么多。
  • 啊,'nonlocal' 关键字正是我想要的,看来 Python 缺少这个。大概这个'级联'通过使用这个关键字导入变量的每个封闭范围?
  • @brainfsck:如果您区分“查找”和“分配”变量,则最容易理解。如果在当前范围内找不到名称,则查找会退回到更高的范围。赋值总是在本地范围内完成(除非你使用globalnonlocal 来强制全局或非本地赋值)
【解决方案2】:

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

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

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

它说它所做的行存在问题的原因是因为它在尝试生成代码之前有效地寻找名称,因此在某种意义上它认为它还没有真正做那行。我认为这是一个可用性错误,但通常学习不要把编译器的消息当回事是一个好习惯。

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

更新,见 cmets:

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

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

c+=1

它把它分解成类似的东西

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

解析器最终想把它变成一个解析树并执行它,但由于它是一个赋值,在它执行之前,它会在本地字典中查找名称 c,没有看到它,并将其插入到字典,将其标记为未初始化。在完全编译的语言中,它只会进入符号表并等待解析,但由于它没有第二遍的奢侈,词法分析器会做一些额外的工作来让以后的生活更轻松。只是,它会看到 OPERATOR,看到规则说“如果你有一个运算符 += 左侧必须已经初始化”,然后说“哎呀!”

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

正如我所说,您可能会说这是一个可用性错误,但它实际上是一个相当普遍的事情。一些编译器对此更诚实,并说“在第 XXX 行或附近出现错误”,但这个没有。

【讨论】:

  • 好的,谢谢您的回复;它为我清除了一些关于 python 范围的事情。但是,我仍然不明白为什么在 (A) 行而不是 (B) 行出现错误。 Python 是否在运行程序之前创建其变量范围字典?
  • 不,是在表达层面。我会补充答案,我认为我不能在评论中加入这个。
  • 关于实现细节的注意事项:在 CPython 中,本地范围通常不作为 dict 处理,它在内部只是一个数组(locals() 将填充一个 dict 以返回,但会更改不要创建新的locals)。解析阶段是找到一个本地的每个分配,并将名称转换为该数组中的位置,并在引用该名称时使用该位置。在进入函数时,非参数局部变量被初始化为占位符,UnboundLocalErrors 发生在读取变量并且其关联索引仍然具有占位符值时。
【解决方案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

    【讨论】:

    • 感谢您的回复,但我认为它不能解释为什么在 (A) 行抛出错误,我只是想打印一个变量。程序永远不会到达试图修改未初始化变量的 (B) 行。
    • Python 将在开始运行程序之前读取、解析并将整个函数转换为内部字节码,因此“将 c 转换为局部变量”这一事实在打印值之后以文本形式发生' t,可以说,很重要。
    【解决方案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】:

      这里有两个链接可能会有所帮助

      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)
      

      【讨论】:

        【解决方案7】:

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

        在大多数情况下,您倾向于认为增强赋值 (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
        

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

        【讨论】:

          【解决方案8】:

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

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

          我还想指出,这与全局范围没有任何关系。使用嵌套函数可以获得相同的行为。

          【讨论】:

            【解决方案9】:

            c+=1 赋值 c,python 假设分配的变量是本地的,但在这种情况下它没有在本地声明。

            使用globalnonlocal 关键字。

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

            my_variables = { # a mutable object
                'c': 3
            }
            
            def test():
                my_variables['c'] +=1
            
            test()
            

            【讨论】:

              【解决方案10】:

              访问类变量的最佳方式是直接按类名访问

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

              【讨论】:

                【解决方案11】:

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

                【讨论】:

                  猜你喜欢
                  相关资源
                  最近更新 更多