当我尝试这段代码:

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期望变量是本地的,但在每种情况下都有阻止赋值的逻辑错误的情况。


当前回答

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

class Employee:
    counter=0

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

其他回答

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

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

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

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

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

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

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

在大多数情况下,你倾向于认为增广赋值(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

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

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