当我尝试这段代码:

a, b, c = (1, 2, 3)

def test():
    print(a)
    print(b)
    print(c)
    c += 1
test()

我从打印(c)行得到一个错误,它说:

UnboundLocalError: local variable 'c' referenced before assignment

在Python的新版本中,或者

UnboundLocalError: 'c' not assigned

在一些老版本中。

如果注释掉c += 1,两次打印都成功。

我不明白:如果c不行,为什么打印a和b可以?c += 1是如何导致print(c)失败的,即使它出现在代码的后面?

赋值c += 1似乎创建了一个局部变量c,它优先于全局变量c。但是一个变量如何在它存在之前“窃取”作用域呢?为什么c是局部的?


请参见在函数中使用全局变量,了解如何从函数中重新分配全局变量的问题,以及是否可以在python中修改位于外部(封闭)但不是全局范围的变量?用于从封闭函数(闭包)重新赋值。

参见为什么不需要'global'关键字来访问全局变量?对于OP预期错误但没有得到错误的情况,从简单地访问一个没有global关键字的全局变量。

参见如何在Python中“解除绑定”名称?什么代码可以导致“UnboundLocalError”?对于OP期望变量是本地的,但在每种情况下都有阻止赋值的逻辑错误的情况。


当前回答

Python对函数中的变量的处理方式不同,这取决于你是在函数内部还是函数外部为变量赋值。如果变量是在函数中赋值的,默认情况下它被视为局部变量。因此,当您取消注释该行时,您将尝试在为局部变量c赋值之前引用它。

如果你想让变量c引用在函数之前赋值的全局c = 3, put

global c

作为函数的第一行。

至于python 3,现在有了

nonlocal c

您可以使用它来引用最近的包含c变量的外围函数作用域。

其他回答

总结

Python decides the scope of the variable ahead of time. Unless explicitly overridden using the global or nonlocal (in 3.x) keywords, variables will be recognized as local based on the existence of any operation that would change the binding of a name. That includes ordinary assignments, augmented assignments like +=, various less obvious forms of assignment (the for construct, nested functions and classes, import statements...) as well as unbinding (using del). The actual execution of such code is irrelevant.

这在文档中也有解释。

讨论

与流行的观点相反,Python在任何意义上都不是一种“解释型”语言。(现在这种情况已经非常罕见了。)Python的参考实现以与Java或c#大致相同的方式编译Python代码:它被转换为虚拟机的操作码(“字节码”),然后模拟虚拟机。其他实现也必须编译代码;否则,eval和exec不能正确地返回一个对象,并且在不实际运行代码的情况下无法检测到SyntaxErrors。

Python如何确定变量作用域

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

如果函数包含一个名称的全局或非局部声明,则该名称将分别被视为引用包含该名称的全局作用域或第一个封闭作用域。 否则,如果它包含任何用于更改名称的绑定(赋值或删除)的语法,即使代码在运行时不会实际更改绑定,该名称也是本地的。 否则,它引用包含该名称的第一个外围作用域,或者引用全局作用域。

Importantly, the scope is resolved at compile time. The generated bytecode will directly indicate where to look. In CPython 3.8 for example, there are separate opcodes LOAD_CONST (constants known at compile time), LOAD_FAST (locals), LOAD_DEREF (implement nonlocal lookup by looking in a closure, which is implemented as a tuple of "cell" objects), LOAD_CLOSURE (look for a local variable in the closure object that was created for a nested function), and LOAD_GLOBAL (look something up in either the global namespace or the builtin namespace).

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

特殊(和非特殊)情况

这里有一些重要的注意事项,请记住语法规则是在编译时实现的,没有静态分析:

It does not matter if the global variable is a builtin function etc., rather than an explicitly created global: def x(): int = int('1') # `int` is local! (Of course, it is a bad idea to shadow builtin names like this anyway, and global cannot help (just like using the same code outside of a function will still cause problems). See https://stackoverflow.com/questions/6039605.) It does not matter if the code could never be reached: y = 1 def x(): return y # local! if False: y = 0 It does not matter if the assignment would be optimized into an in-place modification (e.g. extending a list) - conceptually, the value is still assigned, and this is reflected in the bytecode in the reference implementation as a useless reassignment of the name to the same object: y = [] def x(): y += [1] # local, even though it would modify `y` in-place with `global` However, it does matter if we do an indexed/slice assignment instead. (This is transformed into a different opcode at compile time, which will in turn call __setitem__.) y = [0] def x(): print(y) # global now! No error occurs. y[0] = 1 There are other forms of assignment, e.g. for loops and imports: import sys y = 1 def x(): return y # local! for y in []: pass def z(): print(sys.path) # `sys` is local! import sys Another common way to cause problems with import is trying to reuse the module name as a local variable, like so: import random def x(): random = random.choice(['heads', 'tails']) Again, import is assignment, so there is a global variable random. But this global variable is not special; it can just as easily be shadowed by the local random. Deletion is also changing the name binding, e.g.: y = 1 def x(): return y # local! del y

有兴趣的读者,使用参考实现,鼓励使用dis标准库模块检查这些示例。

外围作用域和nonlocal关键字(在3.x中)

对全局关键字和非局部关键字进行必要的修改后,问题以同样的方式工作。(Python 2。X没有非局部的。)无论采用哪种方式,关键字都需要从外部作用域赋值给变量,但不需要仅仅查找它,也不需要更改所查找的对象。(同样:+=在列表上改变列表,但随后也将名称重新分配给相同的列表。)

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

As seen above, Python does not treat any names as being "in builtin scope". Instead, the builtins are a fallback used by global-scope lookups. Assigning to these variables will only ever update the global scope, not the builtin scope. However, in the reference implementation, the builtin scope can be modified: it's represented by a variable in the global namespace named __builtins__, which holds a module object (the builtins are implemented in C, but made available as a standard library module called builtins, which is pre-imported and assigned to that global name). Curiously, unlike many other built-in objects, this module object can have its attributes modified and deld. (All of this is, to my understanding, supposed to be considered an unreliable implementation detail; but it has worked this way for quite some time now.)

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

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

同样,你得到这个错误的原因是因为你也可以在函数中声明一个与“全局”同名的新变量,并且它将完全独立。解释器认为你试图在这个作用域中创建一个名为c的新变量,并在一个操作中修改它,这在Python中是不允许的,因为这个新的c没有初始化。

Python对函数中的变量的处理方式不同,这取决于你是在函数内部还是函数外部为变量赋值。如果变量是在函数中赋值的,默认情况下它被视为局部变量。因此,当您取消注释该行时,您将尝试在为局部变量c赋值之前引用它。

如果你想让变量c引用在函数之前赋值的全局c = 3, put

global c

作为函数的第一行。

至于python 3,现在有了

nonlocal c

您可以使用它来引用最近的包含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-ref erence

link 1描述了错误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)

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

现在我们得到c+=1,当然,它等价于c=c+1。当Python扫描这一行时,它说:“啊哈,有一个名为c的变量,我将把它放入我的本地作用域字典中。”然后当它在赋值的右边寻找c的值时,它找到了它的局部变量c,它还没有值,因此抛出了错误。

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

它之所以说行中有问题是因为它在尝试生成代码之前有效地查找名称,因此在某种意义上,它认为它还没有真正地执行那一行。我认为这是一个可用性错误,但学习不要太认真地对待编译器的消息通常是一个很好的实践。

如果这能让我感到安慰的话,我花了大约一天的时间来研究这个问题,然后我发现了Guido写的一些关于解释一切的字典的东西。

更新,见评论:

它不会扫描两次代码,但它确实分两个阶段扫描代码:词法分析和解析。

考虑一下如何解析这行代码。词法分析器读取源文本并将其分解为词素(语法的“最小组件”)。所以当它碰到直线时

c+=1

它把它分解成

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

The parser eventually wants to make this into a parse tree and execute it, but since it's an assignment, before it does, it looks for the name c in the local dictionary, doesn't see it, and inserts it in the dictionary, marking it as uninitialized. In a fully compiled language, it would just go into the symbol table and wait for the parse, but since it WON'T have the luxury of a second pass, the lexer does a little extra work to make life easier later on. Only, then it sees the OPERATOR, sees that the rules say "if you have an operator += the left hand side must have been initialized" and says "whoops!"

这里的问题是,它还没有真正开始对行进行解析。这些都是实际解析前的准备工作,所以行计数器还没有执行到下一行。因此,当它发出错误信号时,它仍然认为它在上一行。

就像我说的,你可能会认为这是一个可用性漏洞,但实际上这是一件相当普遍的事情。有些编译器更诚实,会说“第XXX行附近有错误”,但这个编译器没有。