253

以下代码在 Python 2.5 和 3.0 中都按预期工作:

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

print(a, b, c)

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

但是,当我取消注释 line (B)时,我得到一个UnboundLocalError: 'c' not assignedat line (A)a和的值b打印正确。这让我完全困惑有两个原因:

  1. 为什么在(A)行出现运行时错误,因为(B)行后面有一个语句?

  2. 为什么变量a并按b预期打印,而c引发错误?

我能想到的唯一解释是局部变量c是由 assignment 创建的c+=1,它甚至在创建局部变量之前就优先于“全局”变量c。当然,在变量存在之前“窃取”范围是没有意义的。

有人可以解释这种行为吗?

4

11 回答 11

263

Python 以不同的方式处理函数中的变量,具体取决于您是从函数内部还是外部为其赋值。如果在函数中分配了变量,则默认将其视为局部变量。因此,当您取消注释该行时,您试图c在分配任何值之前引用局部变量。

如果您希望变量c引用c = 3函数之前分配的全局,请将

global c

作为函数的第一行。

至于python 3,现在有

nonlocal c

您可以使用它来引用最近的具有c变量的封闭函数范围。

于 2008-12-16T03:12:56.833 回答
86

Python 有点奇怪,因为它将所有内容都保存在字典中,用于各种范围。原始的 a,b,c 在最上面的范围内,因此在最上面的字典中。该函数有自己的字典。当您到达print(a)andprint(b)语句时,字典中没有该名称的任何内容,因此 Python 查找列表并在全局字典中找到它们。

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

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

它说它确实存在问题的原因是因为它在尝试生成代码之前有效地寻找名称,因此从某种意义上说,它认为它还没有真正做到这一点。我认为这是一个可用性错误,但学习不要认真地对待编译器的消息通常是一个好习惯。

如果有什么安慰的话,在我发现 Guido 写的关于解释一切的字典之前,我可能花了一天时间挖掘和试验同样的问题。

更新,见评论:

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

考虑一下这行代码的解析是如何工作的。词法分析器读取源文本并将其分解为词位,即语法的“最小组件”。所以当它击中线时

c+=1

它把它分解成类似的东西

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

解析器最终想把它做成解析树并执行它,但由于它是一个赋值,在它执行之前,它会在本地字典中查找名称 c,没有看到它,并将其插入到字典中,标记它未初始化。在完全编译的语言中,它只会进入符号表并等待解析,但由于它没有第二遍的奢侈,词法分析器会做一些额外的工作来让以后的生活更轻松。只有,然后它会看到 OPERATOR,看到规则说“如果你有一个 operator += 左侧必须已经初始化”并说“哎呀!”

这里的重点是它还没有真正开始解析该行。这一切都在为实际解析做准备,所以行计数器还没有前进到下一行。因此,当它发出错误信号时,它仍然认为它在前一行。

正如我所说,你可以说这是一个可用性错误,但它实际上是一个相当普遍的事情。一些编译器对此更诚实,并说“在第 XXX 行或附近出现错误”,但这个没有。

于 2008-12-16T03:25:29.883 回答
50

看一下反汇编可能会澄清正在发生的事情:

>>> 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 的字节码是LOAD_FAST,而访问 b的字节码是LOAD_GLOBAL。这是因为编译器已经识别出在函数内分配了 a,并将其归类为局部变量。局部变量的访问机制与全局变量根本不同——它们在帧的变量表中静态分配了一个偏移量,这意味着查找是一个快速索引,而不是像全局变量那样更昂贵的字典查找。因此,Python 将这print a一行读取为“获取插槽 0 中保存的局部变量 'a' 的值并打印它”,当它检测到该变量仍未初始化时,会引发异常。

于 2008-12-16T09:49:28.640 回答
11

当您尝试传统的全局变量语义时,Python 有相当有趣的行为。我不记得细节了,但是您可以读取在“全局”范围内声明的变量的值就好了,但是如果要修改它,则必须使用global关键字。尝试更改test()为:

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

此外,您收到此错误的原因是您还可以在该函数内声明一个与“全局”同名的新变量,并且它将完全分开。解释器认为你试图在这个范围内创建一个新变量,c并在一个操作中修改它,这在 Python 中是不允许的,因为这个新变量没有c被初始化。

于 2008-12-16T03:12:59.690 回答
8

最清楚的例子是:

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

调用时,虽然我们永远不会到达 line foo(),但这也会引发 ,因此从逻辑上讲,永远不应该创建局部变量。UnboundLocalErrorbar=0

奥秘在于“ Python 是一种解释性语言”,函数的声明foo被解释为单个语句(即复合语句),它只是对其进行愚蠢的解释并创建局部和全局范围。所以bar在执行之前在本地范围内被识别。

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

这篇文章提供了 Python 变量范围的完整描述和分析:

于 2014-06-04T10:39:21.343 回答
5

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

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

链接一描述了错误 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)
于 2009-11-16T22:12:19.977 回答
3

这不是您问题的直接答案,但它密切相关,因为它是由增强赋值和函数范围之间的关系引起的另一个问题。

在大多数情况下,您倾向于认为扩充赋值 ( a += b) 与简单赋值 ( ) 完全相同a = a + b。但是,在一个极端情况下,可能会遇到一些麻烦。让我解释:

Python 的简单赋值的工作方式意味着 ifa被传递给一个函数(如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

所以诀窍是避免对函数参数进行增强赋值(我尝试只将它用于局部/循环变量)。使用简单的赋值,你就可以避免模棱两可的行为。

于 2009-01-24T15:13:11.677 回答
2

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

我相信您已经知道,在“=”左侧使用的任何名称都隐含地是一个局部变量。不止一次我被改变了对一个 += 的访问,它突然变成了一个不同的变量。

我还想指出这与全局范围没有任何关系。使用嵌套函数可以获得相同的行为。

于 2008-12-16T08:58:10.193 回答
2

c+=1assigns c,python 假设分配的变量是本地的,但在这种情况下,它没有在本地声明。

使用globalornonlocal关键字。

nonlocal仅在 python 3 中有效,因此如果您使用的是 python 2 并且不想将变量设为全局变量,则可以使用可变对象:

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

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

test()
于 2016-11-03T18:52:39.750 回答
1

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

class Employee:
    counter=0

    def __init__(self):
        Employee.counter+=1
于 2015-12-08T10:09:47.367 回答
-1

同样的问题困扰着我。使用nonlocalglobal可以解决问题。
但是,需要注意使用nonlocal,它适用于嵌套函数。但是,在模块级别,它不起作用。请参阅此处的示例

于 2019-09-19T02:26:31.957 回答