Pythonvariables范围错误

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

  1. 为什么在行(A)上有一个运行时错误,因为(B)行后面的语句?

  2. 为什么variablesab按预期打印,而c会产生错误?

唯一可以解释的是, 局部variablescc+=1赋值,即使在创build局部variables之前,它也比“全局”variablesc更为先进。 当然,在variables存在之前,variables“偷窃”范围是没有意义的。

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

Python根据是否从函数内部给它们赋值来区别对待函数中的variables。 如果函数包含对variables的任何赋值,则默认将其视为局部variables。 因此,当您取消注释该行时,您正试图在为其分配任何值之前引用局部variables。

如果你想要variablesc来引用全球c

 global c 

作为函数的第一行。

至于python 3,现在有了

 nonlocal c 

您可以使用引用最近的具有cvariables的封闭函数作用域。

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

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

上面提到的语句global c只是告诉parsing器它使用全局范围中的c ,所以不需要新的c

之所以这样说,是因为它在尝试生成代码之前正在有效地查找名称,所以从某种意义上来说,并不认为它真的在做这个行。 我认为这是一个可用性的错误,但是学习不要认真地对待编译器的消息通常是一个好习惯。

如果有什么安慰的话,我可能花了一天的时间来挖掘和试验这个同样的问题,然后才发现圭多曾经写过关于解释万物的字典。

更新,看评论:

它不扫描代码两次,但它确实在两个阶段扫描代码,lexing和parsing。

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

 c+=1 

它分解成类似的东西

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

parsing器最终希望将其作为一个分析树并执行它,但是由于它是一个赋值,所以在它之前,它会在本地字典中查找名称c,不会看到它,并将其插入字典中,从而标记它作为未初始化。 在一个完全编译的语言中,它只是进入符号表并等待parsing,但由于它不会有第二遍的奢侈,词法分析器会做一些额外的工作,以便稍后使生活更轻松。 只有当它看到操作者时,才会看到规则说“如果你有一个操作员+ =左手边必须被初始化”并且说“哎呀!

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

正如我所说,你可能会认为这是一个可用性错误,但它实际上是一个相当普遍的事情。 有些编译器对此更为诚实,并说“在XXX行左右出现错误”,但这不是。

看看反汇编可能会澄清正在发生的事情:

 >>> 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,并将其分类为局部variables。 对于全局variables来说,局部variables的访问机制是根本不同的 – 它们在variables表中被静态地分配了一个偏移量,这意味着查找是一个快速索引,而不是比较昂贵的词法查找。 因此,Python正在读取print a行为“获取位于0号插槽中的局部variables'a'的值,并将其打印出来”,并且当它检测到该variables仍未初始化时,会引发exception。

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

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

此外,你得到这个错误的原因是因为你也可以在该函数内声明一个新的variables,它与“全局”同名,并且它是完全独立的。 解释器认为你正在试图在这个范围内创build一个新的variablesc并在一个操作中修改它,这在Python中是不允许的,因为这个新的c没有被初始化。

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

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。 链接二可以帮助重写你的testingfunction。 基于链接二,原来的问题可以改写为:

 >>> 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) 

最清楚的例子是:

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

当调用foo() ,这也会引发 UnboundLocalError虽然我们永远不会达到line bar=0 ,所以逻辑上的局部variables永远不会被创build。

神秘之处在于“ Python是一种解释的语言 ”, foo函数的声明被解释为一个单一的语句(即一个复合语句),它只是愚蠢地解释它,创造出局部和全局的范围。 所以在执行之前, bar在本地范围内被识别。

对于更多这样的例子阅读这篇文章: http : //blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-mistakes-part-2/

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

这不是对你的问题的直接回答,而是紧密相关的,因为这是扩大赋值和function范围之间关系的另一个问题。

在大多数情况下,您倾向于将增强赋值( a += b )看作完全等价于简单赋值( a = a + b )。 这个虽然在一个angular落的情况下有可能陷入一些麻烦。 让我解释:

Python的简单赋值方式意味着如果将a传递给函数(如func(a) ;请注意Python总是通过引用传递),那么a = a + b将不会修改传入的函数。相反,它只会修改本地指针。

但是如果你使用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 

所以诀窍是避免函数参数的增加赋值(我试图只用于本地/循环variables)。 使用简单的任务,你会从模棱两可的行为是安全的。

c+=1分配c ,python假定分配的variables是本地的,但在这种情况下,它并没有在本地声明。

使用global或非nonlocal关键字。

nonlocal只能在python 3中运行,所以如果你使用的是python 2,并且不想让你的variables成为全局variables,你可以使用一个可变的对象:

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

Python解释器将作为一个完整的单元读取一个函数。 我认为它是通过两次读取,一次收集它的闭包(局部variables),然后再把它变成字节码。

正如我相信你已经知道,在'='左边使用的任何名字都是一个局部variables。 不止一次,我通过改变对一个+ =的访问而被抓出来,这突然变成了一个不同的variables。

我也想指出,具体与全球范围无关。 你用嵌套函数得到相同的行为。

达到类variables的最好方法是直接通过类名访问

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