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) 。 a
和b
的值被正确打印。 这让我完全困惑,原因有两个:
-
为什么在行(A)上有一个运行时错误,因为(B)行后面的语句?
-
为什么variables
a
和b
按预期打印,而c
会产生错误?
唯一可以解释的是, 局部variablesc
由c+=1
赋值,即使在创build局部variables之前,它也比“全局”variablesc
更为先进。 当然,在variables存在之前,variables“偷窃”范围是没有意义的。
有人可以解释这种行为吗?
Python根据是否从函数内部给它们赋值来区别对待函数中的variables。 如果函数包含对variables的任何赋值,则默认将其视为局部variables。 因此,当您取消注释该行时,您正试图在为其分配任何值之前引用局部variables。
如果你想要variablesc
来引用全球c
放
global c
作为函数的第一行。
至于python 3,现在有了
nonlocal c
您可以使用引用最近的具有c
variables的封闭函数作用域。
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