Python variable scope error
以下代码在python 2.5和3.0中都能正常工作:
1 2 3 4 5 6 7 8 9 10 | a, b, c = (1, 2, 3) print(a, b, c) def test(): print(a) print(b) print(c) # (A) #c+=1 # (B) test() |
但是,当我取消对(b)行的注释时,我在(a)行得到一个
为什么第(a)行由于第(b)行上的稍后语句而引发运行时错误?
为什么变量
我唯一能想到的解释是,局部变量
有人能解释一下这种行为吗?
python对函数中的变量的处理方式不同,这取决于是否从函数中为变量赋值。如果函数包含对变量的任何赋值,则默认情况下它被视为局部变量。因此,当取消对行的注释时,您试图在为局部变量赋值之前引用它。
如果希望变量
1 | global c |
作为函数的第一行。
至于python 3,现在有了
1 | nonlocal c |
您可以用来引用具有
Python有点奇怪,因为它将所有内容都保存在字典中,用于各种范围。原来的A,B,C在最上面的范围内,所以在最上面的字典里。函数有自己的字典。当您到达
现在我们到了
上面提到的声明
它之所以说这行中存在问题,是因为它在尝试生成代码之前有效地查找了名称,所以在某种意义上,它还没有真正做到这一行。我认为这是一个可用性缺陷,但一般来说,学习不要太认真地对待编译器的消息是一个很好的实践。
如果有什么安慰的话,我可能花了一天的时间来研究和试验同一个问题,然后才发现吉多写了一些解释一切的字典。
更新,请参见注释:它不会扫描代码两次,但它会分两个阶段扫描代码,即词法分析和解析。
考虑这一行代码的解析方式。lexer读取源文本并将其分解为lexems,这是语法的"最小组件"。所以当它碰到线的时候
1 | c+=1 |
它把它分解成
1 | SYMBOL(c) OPERATOR(+=) DIGIT(1) |
解析器最终希望把它变成一个解析树并执行它,但是由于它是一个赋值,所以在它执行之前,它会在本地字典中查找名称c,看不到它,并将其插入字典,将其标记为未初始化。在完全编译的语言中,它只需进入符号表并等待解析,但是由于它没有第二次传递的奢侈,lexer做了一些额外的工作,以使以后的生活更轻松。只有这样,它才能看到运算符,看到规则说"如果有运算符+=左手边必须已初始化",然后说"哎呀!"
这里的重点是,它还没有真正开始解析行。这一切都是为实际的解析做准备的,所以行计数器没有前进到下一行。因此,当它发出错误信号时,它仍然认为它在前一行。
正如我所说,你可以说这是一个可用性缺陷,但实际上这是一个相当常见的问题。有些编译器对此更诚实,并说"行上或行附近的错误",但这一个没有。
看一看拆卸可能会弄清楚发生了什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | >>> 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的字节码是
当您尝试传统的全局变量语义时,python有相当有趣的行为。我不记得详细信息,但是您可以读取"global"范围中声明的变量的值,但如果您想修改它,则必须使用
1 2 3 4 5 6 | def test(): global c print(a) print(b) print(c) # (A) c+=1 # (B) |
另外,之所以出现此错误,是因为您还可以在该函数中声明一个与"全局"变量同名的新变量,并且该变量将完全独立。解释器认为您正试图在此范围内生成一个名为
最好的例子是:
1 2 3 4 5 | bar = 42 def foo(): print bar if False: bar = 0 |
当调用
神秘之处在于"python是一种解释语言",函数
如需了解更多此类示例,请阅读以下文章:http://blog.amir.rachum.com/blog/2013/07/09/python-common-newbie-errors-part-2/
本文提供了对变量python作用域的完整描述和分析:
这里有两个链接可以帮助
1:docs.python.org/3.1/faq/programming.html?highlight=nonlocal why-am-i-getting-an-unboundlocalerror-when-variable-has-a-value
2:docs.python.org/3.1/faq/programming.html?highlight=非本地how-do-i-write-a-function-with-output-parameters-call-by-reference
链接1描述了UnboundLocalError错误。链接2可以帮助您重新编写测试函数。基于链接2,原始问题可以改写为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | >>> 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) |
这不是对您的问题的直接回答,但它是密切相关的,因为它是由增强赋值和函数作用域之间的关系引起的另一个gotcha。
在大多数情况下,您倾向于认为增强赋值(
python的简单赋值方式意味着,如果
但是,如果您使用
1 | a = a + b |
或者有时(如果方法存在)为:
1 | a.__iadd__(b) |
在第一种情况下(只要
在第二种情况下,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 |
所以技巧是避免对函数参数进行增广赋值(我尝试只对局部/循环变量使用它)。使用简单的作业,你就不会有模棱两可的行为。
使用
1 2 3 4 5 6 7 8 | my_variables = { # a mutable object 'c': 3 } def test(): my_variables['c'] +=1 test() |
python解释器将作为一个完整的单元来读取函数。我认为它是在两个过程中读取的,一次是收集它的闭包(局部变量),然后再次将其转换为字节代码。
正如我确信您已经知道的,在"="左边使用的任何名称都是隐式局部变量。不止一次,我被一个变量的访问权变成了一个+=而它突然变成了一个不同的变量。
我还想指出,具体来说,这与全球范围无关。使用嵌套函数可以获得相同的行为。
到达类变量的最佳方法是通过类名直接访问
1 2 3 4 5 | class Employee: counter=0 def __init__(self): Employee.counter+=1 |
在Python中,我们对所有类型的变量(局部变量、类变量和全局变量)都有类似的声明。当您从方法引用全局变量时,python认为您实际上是从方法本身引用变量,而方法本身尚未定义,因此引发了错误。要引用全局变量,我们必须使用globals()['variablename']。
在您的情况下,分别使用globals()['a]、globals()['b']和globals()['c']而不是a、b和c。