【问题标题】:UnboundLocalError on local variable when reassigned after first use首次使用后重新分配时局部变量出现 UnboundLocalError
【发布时间】:2022-12-11 19:15:52
【问题描述】:

以下代码在 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()

但是,当我取消注释行时(二),我在行中得到一个UnboundLocalError: 'c' not assigned(一个). ab 的值打印正确。我不明白:

  1. 为什么会出现运行时错误(一个)因为后来网上有说法(二)

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

    我能想到的唯一解释是赋值 c+=1 创建了一个当地的变量c,优先于全局变量c。但是变量如何在它存在之前“窃取”作用域呢?为什么 c 显然是本地的?


    另请参阅Using global variables in a function,了解有关如何从函数内重新分配全局变量的问题,以及Is it possible to modify variable in python that is in outer, but not global, scope?,了解如何从封闭函数(闭包)重新分配。有关 OP 的情况,请参阅Why isn't the 'global' keyword needed to access a global variable?预期的一个错误但是没有得到一个,通过简单地访问一个没有 global 关键字的全局。

【问题讨论】:

标签: python variables scope


【解决方案1】:

Python 以不同方式处理函数中的变量,具体取决于您是从函数内部还是外部为它们赋值。如果一个变量在一个函数中赋值,它默认被视为一个局部变量。因此,当您取消注释该行时,您是在尝试引用局部变量c,然后再为其分配任何值。

如果你想让变量c引用函数之前分配的全局c = 3,把

global c

作为函数的第一行。

至于python 3,现在有

nonlocal c

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

【讨论】:

  • 谢谢。快速提问。这是否意味着 Python 在运行程序之前决定了每个变量的范围?在运行功能之前?
  • 变量作用域由编译器决定,它通常在您第一次启动程序时运行一次。然而,值得记住的是,如果您的程序中有“eval”或“exec”语句,编译器也可能稍后运行。
  • 好的谢谢你。我想“解释性语言”并不像我想象的那么多。
  • 啊,那个“非本地”关键字正是我要找的,Python 似乎缺少这个。大概这个“级联”通过使用 this 关键字导入变量的每个封闭范围?
  • @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 写的关于 Explained Everything 的词典的内容之前,我可能花了一天的时间来挖掘和试验同一个问题。

更新,查看cmets:

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

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

c+=1

它把它分解成类似的东西

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

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

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

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

【讨论】:

  • 关于实现细节的注意事项:在 CPython 中,本地范围通常不作为 dict 处理,它在内部只是一个数组(locals() 将填充 dict 以返回,但对其的更改不会创建新的 @ 987654332@)。解析阶段是找到对本地的每个赋值并将名称转换为该数组中的位置,并在引用名称时使用该位置。在进入函数时,非参数局部变量被初始化为占位符,并且当读取变量并且其关联索引仍然具有占位符值时发生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'范围内声明的变量的值就好了,但是如果你想修改它,你必须使用global关键字。尝试将 test() 更改为:

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

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

    【讨论】:

    • 感谢您的回复,但我认为这并不能解释为什么在第 (A) 行抛出错误,我只是在该行尝试打印一个变量。该程序永远不会到达试图修改未初始化变量的行 (B)。
    • Python 将在开始运行程序之前读取、解析并将整个函数转换为内部字节码,因此“将 c 转换为局部变量”这一事实在打印值之后以文本方式发生并不重要。
    • Python 允许您访问局部范围内的全局变量以进行读取,但不能进行写入。这个答案有一个很好的解决方法,在下面的评论中有解释...... +=1。
    【解决方案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 变量作用域的完整描述和分析:

    【讨论】:

    • Python 并不比 Java 或 C# 更“解释”,实际上决定将 bar 视为此代码中的局部变量需要前期编译步骤。
    【解决方案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 assigns c,python 假设分配的变量是本地的,但在这种情况下它没有在本地声明。

          使用globalnonlocal 关键字。

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

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

          【讨论】:

            【解决方案10】:

            与流行的看法相反,Python 在任何意义上都不是一种“解释型”语言。这些现在已经非常罕见了。 Python 的参考实现以与 Java 或 C# 大致相同的方式编译 Python 代码:它被翻译成操作码(“字节码”)用于虚拟机,然后进行仿真。其他实现也必须编译代码;否则,evalexec 无法正确返回对象,SyntaxErrors 在没有实际运行代码的情况下无法检测到。

            在编译期间(无论是否在参考实现上),Python follows simple rules 用于决定函数中的变量范围:

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

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

            • 否则,它指的是包含名称的第一个封闭范围,否则是全局范围。 (当在全局作用域中查找名称时,内置作用域将在运行时作为回退检查;对内置作用域中的名称的赋值将在全局作用域中赋值。)

            这里的重点是作用域被解析在编译时.生成的字节码将直接指示要查找的位置。例如,在 CPython 3.8 中,有单独的操作码 LOAD_CONST(编译时已知的常量)、LOAD_FAST(局部变量)、LOAD_DEREF(通过在闭包中查找来实现 nonlocal 查找,闭包实现为元组“cell”对象)、LOAD_CLOSURE(在为嵌套函数创建的闭包对象中查找局部变量)和LOAD_GLOBAL(在全局命名空间或内置命名空间中查找一些东西)。

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


            这里有一些重要的考虑因素,请记住语法规则是在编译时实现的,没有静态分析:

            • 不要紧如果永远无法访问代码:
              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`
              
            • 然而,它如果我们改为进行索引/切片分配,则很重要。 (这在编译时被转换成不同的操作码,它将依次调用__getitem__。)
              y = [0]
              def x():
                  print(y) # global now! No error occurs.
                  y[0] = 1
              
            • 还有其他形式的赋值,例如:
              y = 1
              def x():
                  return y # local!
                  for y in []:
                      pass
              
            • 删除也会更改名称绑定,例如:
              y = 1
              def x():
                  return y # local!
                  del y
              

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


            问题的工作方式相同,比照, 对于 globalnonlocal 关键字。 (Python 2.x does not have nonlocal。)无论哪种方式,关键字都是从外部范围分配给变量所必需的,但是不是有必要只是查一下, 也不是变异查找的对象。 (同样:列表中的+= 改变了列表,但是然后也重新分配同一个列表中的名称。)

            【讨论】:

              【解决方案11】:

              访问类变量的最好方法是直接通过类名访问

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

              【讨论】:

                【解决方案12】:

                如果您定义一个与方法同名的变量,您也会收到此消息。

                例如:

                def teams():
                    ...
                
                def some_other_method():
                    teams = teams()
                

                解决方案是将方法teams() 重命名为get_teams() 之类的其他名称。

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

                你最终会得到这样的东西来解决它:

                def teams():
                    ...
                
                def some_other_method():
                    teams = get_teams()
                

                【讨论】:

                  【解决方案13】:

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

                  【讨论】:

                    猜你喜欢
                    • 2021-11-29
                    相关资源
                    最近更新 更多