当我尝试这段代码:

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有相当有趣的行为。我不记得细节了,但你可以读取在'global'范围内声明的变量的值,但如果你想修改它,你必须使用global关键字。试着把test()改成这样:

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

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


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行附近有错误”,但这个编译器没有。


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

我相信你已经意识到,在'='左边使用的任何名称都是隐式的局部变量。我不止一次因为改变一个变量对a +=的访问而被发现,它突然变成了一个不同的变量。

我还想指出的是,它与全局作用域并没有什么特别的关系。嵌套函数也有同样的行为。


看一看这个分解程序可能会清楚发生了什么:

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

As you can see, the bytecode for accessing a is LOAD_FAST, and for b, LOAD_GLOBAL. This is because the compiler has identified that a is assigned to within the function, and classified it as a local variable. The access mechanism for locals is fundamentally different for globals - they are statically assigned an offset in the frame's variables table, meaning lookup is a quick index, rather than the more expensive dict lookup as for globals. Because of this, Python is reading the print a line as "get the value of local variable 'a' held in slot 0, and print it", and when it detects that this variable is still uninitialised, raises an exception.


这不是对你的问题的直接回答,但它是密切相关的,因为这是由增广赋值和函数作用域之间的关系引起的另一个问题。

在大多数情况下,你倾向于认为增广赋值(a += b)与简单赋值(a = a + b)完全等价。不过,在一个角落的情况下,这可能会遇到一些麻烦。让我解释一下:

Python简单赋值的工作方式意味着,如果a被传递给一个函数(如func(a);注意Python总是引用传递),那么a = a + b将不会修改传入的a。相反,它只会修改指向a的局部指针。

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

因此,诀窍是避免对函数参数进行扩展赋值(我尝试只对局部/循环变量使用它)。使用简单的赋值,就不会出现模棱两可的行为。


这里有两个链接可能会有所帮助

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)

最好的例子是:

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

当调用foo()时,这也会引发UnboundLocalError,尽管我们永远不会到达line bar=0,所以逻辑上不应该创建本地变量。

神秘之处在于“Python是一种解释型语言”,函数foo的声明被解释为一个单独的语句(即复合语句),它只是无声地解释它,并创建局部和全局作用域。因此bar在执行前在局部范围内被识别。

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

这篇文章提供了对Python变量作用域的完整描述和分析:


访问类变量的最佳方法是直接通过类名访问

class Employee:
    counter=0

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

C +=1赋值C, python假设赋值的变量是本地的,但在这种情况下,它没有在本地声明。

使用全局或非本地关键字。

Nonlocal只在python 3中有效,所以如果你在使用python 2并且不想让你的变量为全局变量,你可以使用一个可变对象:

my_variables = { # a mutable object
    'c': 3
}

def test():
    my_variables['c'] +=1

test()

如果定义了与方法同名的变量,也可以得到此消息。

例如:

def teams():
    ...

def some_other_method():
    teams = teams()

解决方案是将方法teams()重命名为get_teams()。

因为它只在本地使用,所以Python消息相当具有误导性!

你最终会得到这样的结果:

def teams():
    ...

def some_other_method():
    teams = get_teams()

在初始化之后,通常在循环或条件块中对变量使用del关键字时,也会发生此问题。


总结

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


在下面n = num的情况下,n是一个局部变量,num是一个全局变量:

num = 10

def test():
  # ↓ Local variable
    n = num
       # ↑ Global variable
    print(n)
  
test()

因此,没有错误:

10

但是在下面的num = num的情况下,两边的num是局部变量,右边的num还没有定义:

num = 10

def test():
   # ↓ Local variable
    num = num
         # ↑ Local variable not defined yet
    print(num)
  
test()

所以,有下面的错误:

UnboundLocalError:赋值前引用的本地变量'num'

此外,即使删除num = 10,如下所示:

# num = 10 # Removed

def test():
   # ↓ Local variable
    num = num
         # ↑ Local variable not defined yet
    print(num)
  
test()

下面是同样的错误:

UnboundLocalError:赋值前引用的本地变量'num'

因此,为了解决上述错误,将global num放在num = num之前,如下所示:

num = 10

def test():
    global num # Here
    num = num 
    print(num)
  
test()

这样,上述误差就得到了解决,如下图所示:

10

或者,在num = num之前定义局部变量num = 5,如下所示:

num = 10

def test():
    num = 5 # Here
    num = num
    print(num)
  
test()

这样,上述误差就得到了解决,如下图所示:

5