【问题标题】:Variable scope in nested functions嵌套函数中的变量范围
【发布时间】:2013-05-03 09:46:52
【问题描述】:

谁能解释一下为什么下面的程序会失败:

def g(f):
  for _ in range(10):
    f()

def main():
  x = 10
  def f():
    print x
    x = x + 1
  g(f)

if __name__ == '__main__':
  main()

带有消息:

Traceback (most recent call last):
  File "a.py", line 13, in <module>
    main()
  File "a.py", line 10, in main
    g(f)
  File "a.py", line 3, in g
    f()
  File "a.py", line 8, in f
    print x
UnboundLocalError: local variable 'x' referenced before assignment

但如果我只是将变量 x 更改为数组,它就可以工作:

def g(f):
  for _ in range(10):
    f()

def main():
  x = [10]
  def f():
    print x[0]
    x[0] = x[0] + 1
  g(f)

if __name__ == '__main__':
  main()

输出

10
11
12
13
14
15
16
17
18
19

我感到困惑的原因是,如果从f() 无法访问x,那么如果x 是一个数组,为什么它可以访问?

谢谢。

【问题讨论】:

    标签: python scope nested-function


    【解决方案1】:

    但是这个答案说问题在于分配给 x。如果是这样的话, 那么打印它应该可以正常工作,不是吗?

    您必须了解事情发生的顺序。在你的 python 代码甚至被编译和执行之前,一个叫做 parser 的东西会读取 python 代码并检查语法。解析器所做的另一件事是将变量标记为本地变量。当解析器在本地范围内的代码中看到赋值时,赋值左侧的变量被标记为本地。那时,甚至还没有编译任何东西——更不用说执行了,因此没有发生赋值;该变量仅被标记为局部变量。

    解析器完成后,代码被编译并执行。当执行到达打印语句时:

    def main():
      x = 10     #<---x in enclosing scope
    
      def f():
        print x    #<-----
    
        x = x + 1  #<-- x marked as local variable inside the function f()
    

    print 语句看起来应该继续并在 enclosure 范围内打印 x(LEGB 查找过程中的“E”)。然而,因为解析器之前将 x 标记为 f() 中的局部变量,python 不会继续越过局部范围(LEGB 查找过程中的“L”)来查找 x。因为在执行 'print x' 时 x 尚未在本地范围内分配,所以 python 吐出一个错误。

    请注意,即使发生赋值的代码永远不会执行,解析器仍然会将赋值左侧的变量标记为局部变量。解析器不知道事情将如何执行,因此它盲目地在整个文件中搜索语法错误和局部变量——即使是在永远无法执行的代码中。以下是一些例子:

    def dostuff ():
        x = 10 
    
        def f():
            print x
    
            if False:  #The body of the if will never execute...
                a b c  #...yet the parser finds a syntax error here
    
    
        return f
    
    f = dostuff()
    f()
    
    
    
    --output:--
    File "1.py", line 8
         a b c
          ^
    SyntaxError: invalid syntax
    

    解析器在标记局部变量时做同样的事情:

    def dostuff ():
        x = 10 
    
        def f():
            print x
    
            if False:  #The body of the if will never execute...
                x = 0  #..yet the parser marks x as a local variable
    
        return f
    
    f = dostuff()
    f()
    

    现在看看当你执行最后一个程序时会发生什么:

    Traceback (most recent call last):
      File "1.py", line 11, in <module>
        f()
      File "1.py", line 4, in f
        print x
    UnboundLocalError: local variable 'x' referenced before assignment
    

    当语句 'print x' 执行时,由于解析器将 x 标记为局部变量,因此对 x 的查找将在局部范围内停止。

    这个“特性”并不是 python 独有的——它也出现在其他语言中。

    至于数组的例子,你写的时候:

    x[0] = x[0] + 1
    

    告诉 python 去查找一个名为 x 的数组并为它的第一个元素分配一些东西。因为在局部范围内没有对任何名为 x 的东西进行赋值,所以解析器不会将 x 标记为局部变量。

    【讨论】:

    • +1 用于提及编译器以解释作用域的工作原理。我的新观点。
    【解决方案2】:

    原因是在第一个示例中,您使用了赋值操作x = x + 1,因此在定义函数时,python 认为x 是局部变量。但是,当您实际调用函数时,python 未能在本地 RHS 上找到 x 的任何值,因此引发了错误。

    在您的第二个示例中,您只是更改了一个可变对象而不是赋值,因此 python 永远不会提出任何异议,并将从封闭范围中获取 x[0] 的值(实际上它首先在封闭范围中查找它,然后全局作用域,最后在内置函数中,但一被发现就停止)。

    在 python 3x 中,您可以使用 nonlocal 关键字来处理此问题,而在 py2x 中,您可以将值传递给内部函数或使用函数属性。

    使用函数属性:

    def main():
      main.x = 1
      def f():
          main.x = main.x + 1
          print main.x
      return f
    
    main()()   #prints 2
    

    显式传递值:

    def main():
      x = 1
      def f(x):
          x = x + 1
          print x
          return x
      x = f(x)     #pass x and store the returned value back to x
    
    main()   #prints 2
    

    在 py3x 中使用nonlocal

    def main():
      x = 1
      def f():
          nonlocal x
          x = x + 1
          print (x)
      return f
    
    main()()  #prints 2
    

    【讨论】:

    • 函数属性不推荐。由于该属性只有一份副本,因此任何递归都会导致意想不到的结果。
    【解决方案3】:

    问题是变量x 被闭包拾取。当您尝试分配从闭包中提取的变量时,python 会抱怨除非您使用 globalnonlocal1 关键字。在您使用list 的情况下,您没有分配名称——您可以修改在闭包中拾取的对象,但不能分配给它。


    基本上,错误发生在print x行,因为当python解析函数时,它看到x被分配了,所以它假设x必须是一个局部变量。当您到达print x 行时,python 会尝试查找本地x,但它不存在。这可以通过使用dis.dis 检查字节码来查看。这里,python 使用LOAD_FAST 指令用于局部变量,而不是LOAD_GLOBAL 指令用于非局部变量。

    通常,这会导致NameError,但python 尝试通过在func_closurefunc_globals 2 中查找x 来提供更多帮助。如果它在其中一个中找到x,它会引发UnboundLocalError,而不是让您更好地了解正在发生的事情——您有一个找不到的局部变量(不是“绑定”)。

    1仅限python3.x

    2python2.x -- 在 python3.x 上,这些属性已分别更改为 __closure____globals__

    【讨论】:

    • 但是为什么它在print x 而不是x = x + 1 失败呢?
    • @icando: print x 先来?
    • @rynah:但这个答案表明问题在于 分配x。如果是这样,那么printing 它应该可以正常工作,不是吗?
    • 问题在于读取您分配给的变量,但在您分配给它之前。 Python 提前“知道”您在函数中的某处分配给 x,因此它假定 x 是本地的,但是当您在为它分配值之前尝试从中读取时,这是一个错误。
    • @icando -- 很抱歉对此的回复晚了。我的电脑开始出现问题,所以直到现在我才能详细说明。
    【解决方案4】:

    问题出在一行

    x = x + 1
    

    这是第一次在函数f() 中分配x,告诉编译器x 是一个本地名称。它与上一行print x 冲突,后者找不到本地x 的任何先前分配。 这就是您的错误UnboundLocalError: local variable 'x' referenced before assignment 的来源。

    请注意,当编译器试图找出xprint x 中引用的对象时,就会发生错误。所以print x 不会执行。

    改成

    x[0] = x[0] + 1
    

    没有添加新名称。所以编译器知道你指的是f()之外的数组。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-07-02
      • 2011-07-10
      • 2016-09-25
      • 2012-06-24
      • 1970-01-01
      • 2019-08-21
      • 2016-02-19
      • 2019-04-30
      相关资源
      最近更新 更多