当我尝试这段代码:

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有相当有趣的行为。我不记得细节了,但你可以读取在'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变量的外围函数作用域。

在下面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

当您尝试传统的全局变量语义时,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 +=的访问而被发现,它突然变成了一个不同的变量。

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