Python变量范围错误

tba*_*tba 195 python variables scope

以下代码在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()
Run Code Online (Sandbox Code Playgroud)

但是,当我取消注释行(B)时,我得到了UnboundLocalError: 'c' not assigned一行(A).的值ab被正确地打印.这让我感到困惑,原因有两个:

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

  2. 为什么变量ab打印符合预期,同时c引发错误?

我能想到的唯一解释是,赋值创建了一个局部变量,即使在创建局部变量之前,它也优先于"全局"变量.当然,变量在存在之前"窃取"范围是没有意义的.cc+=1c

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

rec*_*ive 204

Python会根据您是否从函数中为它们赋值来不同地处理函数中的变量.如果函数包含对变量的任何赋值,则默认情况下将其视为局部变量.因此,当您取消注释该行时,您将尝试在为其分配任何值之前引用局部变量.

如果您希望变量c引用全局cput

global c
Run Code Online (Sandbox Code Playgroud)

作为功​​能的第一行.

至于python 3,现在有

nonlocal c
Run Code Online (Sandbox Code Playgroud)

您可以用来引用具有c = 3变量的最近的封闭函数范围.

  • 变量范围决策由编译器决定,编译器通常在您第一次启动程序时运行一次.但是,值得记住的是,如果程序中包含"eval"或"exec"语句,编译器也可能会稍后运行. (7认同)
  • @brainfsck:如果你区分"查找"和"分配"变量,最容易理解.如果在当前范围中找不到该名称,则查找会回退到更高的范围.赋值始终在本地范围内完成(除非您使用`global`或`nonlocal`强制全局或非本地赋值) (6认同)
  • 谢谢.快问.这是否意味着Python在运行程序之前决定每个变量的范围?在运行功能之前? (3认同)
  • 好的谢谢你.我猜"解释语言"并不像我想象的那么多. (2认同)

Cha*_*tin 69

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

现在我们到达c+=1,当然,相当于c=c+1.当Python扫描该行时,它会显示"aha,有一个名为c的变量,我会将它放入我的本地范围字典中." 然后,当它为赋值右侧的c寻找c的值时,它会找到名为c的局部变量,该变量尚无值,因此抛出错误.

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

它之所以说它出现问题的原因是因为它在尝试生成代码之前有效地寻找名称,因此在某种意义上它并不认为它确实在做那条线.我认为这是一个可用性错误,但通常一个好习惯就是学会不要认真地对待编译器的消息.

如果有任何安慰,我可能花了一天时间挖掘并尝试同样的问题,然后才发现Guido写的关于解释一切的词典.

更新,看评论:

它不会扫描代码两次,但它会分两个阶段扫描代码,lexing和parsing.

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

c+=1
Run Code Online (Sandbox Code Playgroud)

它把它分解成类似的东西

SYMBOL(c) OPERATOR(+=) DIGIT(1)
Run Code Online (Sandbox Code Playgroud)

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

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

正如我所说,你可以说这是一个可用性错误,但它实际上是一个相当普遍的事情.有些编译器对此更加诚实,并说"XXX线上或附近有错误",但这个没有.

  • @CharlieMartin很好的回答和btw +1为你的个人资料评论马丁先生!让我的一天:D (5认同)
  • 关于实现细节的注意事项:在CPython中,本地范围通常不作为`dict`处理,它在内部只是一个数组(`locals()`将填充一个`dict`返回,但是对它的更改不会创建新的"当地人").解析阶段是查找对本地的每个赋值,并从该名称转换到该数组中的位置,并在引用该名称时使用该位置.在进入函数时,非参数局部化初始化为占位符,并且当读取变量且其关联索引仍具有占位符值时,会发生"UnboundLocalError". (2认同)

Bri*_*ian 43

看一下反汇编可能会澄清发生了什么:

>>> 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
Run Code Online (Sandbox Code Playgroud)

如您所见,用于访问a的字节码是LOAD_FAST和b LOAD_GLOBAL.这是因为编译器已经识别出在函数内分配了a,并将其归类为局部变量.本地化的访问机制对于全局变量是根本不同的 - 它们在帧的变量表中静态分配了一个偏移量,这意味着查找是一个快速索引,而不是像全局变量那样更昂贵的dict查找.正因为如此,Python正在读取该print a行"获取局部变量的值'a'保存在插槽0中并打印它",并且当它检测到该变量仍然未初始化时,引发异常.

  • 我以前没见过dis模块.有趣的东西 - 谢谢你:-) (9认同)

Mon*_*ose 10

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

def test():
    global c
    print(a)
    print(b)
    print(c)    # (A)
    c+=1        # (B)
Run Code Online (Sandbox Code Playgroud)

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


Sah*_*lra 6

明确的最好例子是:

bar = 42
def foo():
    print bar
    if False:
        bar = 0
Run Code Online (Sandbox Code Playgroud)

在调用时foo(),这也会引发 UnboundLocalError虽然我们永远不会到达行bar=0,所以永远不应该创建逻辑局部变量.

神秘之处在于" Python是一种解释语言 ",函数的声明foo被解释为单个语句(即复合语句),它只是笨拙地解释它并创建局部和全局范围.因此bar在执行前在本地范围内被识别.

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

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


mcd*_*don 5

这里有两个可能有用的链接

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)
Run Code Online (Sandbox Code Playgroud)


Kar*_*tel 5

概括

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

文档中也对此进行了解释。

讨论

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

Python 如何确定变量范围

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

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

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

  • 否则,它指的是包含该名称的第一个封闭范围,否则指的是全局范围。

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

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

特殊(和非特殊)情况

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

  • 全局变量是否是内置函数等,而不是显式创建的全局变量, 这并不重要:
    def x():
        int = int('1') # `int` is local!
    
    Run Code Online (Sandbox Code Playgroud) (当然,无论如何,像这样隐藏内置名称都是一个坏主意,并且global无济于事 - 就像在函数外部使用相同的代码仍然会导致问题一样。)
  • 如果永远无法访问该代码 也没关系
    y = 1
    def x():
        return y # local!
        if False:
            y = 0
    
    Run Code Online (Sandbox Code Playgroud)
  • 如果分配被优化为就地修改(例如扩展列表)并不重要 - 从概念上讲,值仍然被分配,并且这在参考实现的字节码中反映为无用的名称重新分配同一个对象:
    y = []
    def x():
        y += [1] # local, even though it would modify `y` in-place with `global`
    
    Run Code Online (Sandbox Code Playgroud)
  • 然而,如果我们进行索引/切片分配,这确实很重要。(这在编译时转换为不同的操作码,依次调用__setitem__。)
    y = [0]
    def x():
        print(y) # global now! No error occurs.
        y[0] = 1
    
    Run Code Online (Sandbox Code Playgroud)
  • 还有其他形式的赋值,例如for循环和imports:
    import sys
    
    y = 1
    def x():
        return y # local!
        for y in []:
            pass
    
    def z():
        print(sys.path) # `sys` is local!
        import sys
    
    Run Code Online (Sandbox Code Playgroud)
  • 导致问题的另一种常见方法import是尝试将模块名称重用为局部变量,如下所示:
    import random
    
    def x():
        random = random.choice(['heads', 'tails'])
    
    Run Code Online (Sandbox Code Playgroud) 再次,import是赋值,所以有一个全局变量random。但这个全局变量并不特殊;它很容易被本地的阴影所掩盖random
  • 删除也会改变名称绑定,例如:
    y = 1
    def x():
        return y # local!
        del y
    
    Run Code Online (Sandbox Code Playgroud)

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

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

对于和关键字,该问题的工作方式相同(经过必要的修改)。(Python 2.x没有。)无论哪种方式,关键字都是从外部作用域分配给变量所必需的,但不需要仅仅查找,也不需要改变查找的对象。(再次强调:在列表上会改变列表,但也会将名称重新分配给同一个列表。)globalnonlocalnonlocal+=

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

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


归档时间:

查看次数:

61294 次

最近记录:

5 年,11 月 前