为什么Python中的函数可以打印封闭作用域中的变量,但不能在赋值中使用它们?

33
如果我运行以下代码:
x = 1

class Incr:
    print(x)
    x = x + 1
    print(x)

print(x)

它会打印出:

1
2
1

好的,没问题,这正是我所预期的。如果我按照以下方式操作:

x = 1

class Incr:
    global x
    print(x)
    x = x + 1
    print(x)

print(x)

它打印:

1
2
2

也是我预期的。没有问题。

现在,如果我按照以下方式开始制作一个自增函数:

x = 1

def incr():
    print(x)

incr()

它像我预期的那样打印1。我认为这是因为它在其本地范围内找不到x,因此搜索其封闭范围并在那里找到x。到目前为止还没有问题。

现在,如果我执行:

x = 1

def incr():
    print(x)
    x = x + 1

incr()
这在 traceback 中给我以下错误:
UnboundLocalError: local variable 'x' referenced before assignment.
当 Python 在赋值时找不到要使用的 x 的值时,为什么不会像我的 class Incr 一样搜索封闭空间中的 x?请注意,我不是在问如何让这个函数工作。如果我执行以下操作,我知道该函数将正常运行:
x = 1

def incr():
    global x
    print(x)
    x = x + 1
    print(x)

incr()

这将正确打印:

1
2

就像我预期的一样。我想问的是,为什么当关键字 global 不出现时,它不会像上面我的类那样从封闭的作用域中提取 x。既然函数能够读取并打印出 x 的值,我知道它已经将 x 作为封闭作用域的一部分...那为什么这个方法不能像类的例子一样工作呢?为什么解释器需要将其报告为 UnboundLocalError,而显然它知道某些 x 是存在的。

为什么将 x 的值用于打印与将其值用于赋值如此不同?我真的不明白。


这是Python2还是Python3? - Ethan Furman
你考虑过在incr()函数中使用x+=1吗?__add____iadd__之间可能会有所不同。 - inspectorG4dget
@Ethan 这是Python 2.75版本。我使用括号是因为最近我经常切换语言,这样更加统一。 - Shashank
如果您尝试x+=1,将会得到相同的结果。 - Shashank
由于第一个链接的重复,该函数无法正常工作。但是,由于类不会创建自己的作用域,所以它可以与类一起使用,因此局部变量无处可去,也就不会引起问题。这在第二个链接的重复中有解释。 - Karl Knechtel
7个回答

46
类和函数是不同的,类内部的变量实际上被分配到类的命名空间作为其属性,而在函数内部,变量只是普通的变量,不能在外部访问。
函数内的局部变量实际上是在函数第一次解析时确定的,Python 不会在全局范围内搜索它们,因为它知道你将其声明为局部变量。
因此,一旦 Python 看到一个 x = x + 1(赋值)并且没有为该变量声明 global,那么 Python 就不会在全局或其他范围中查找该变量。
>>> x = 'outer'
>>> def func():
...     x = 'inner'  #x is a local variable now
...     print x
...     
>>> func()
inner

常见错误:

>>> x = 'outer'
>>> def func():
...     print x       #this won't access the global `x`
...     x = 'inner'   #`x` is a local variable
...     print x
...     
>>> func()
...
UnboundLocalError: local variable 'x' referenced before assignment

但是当您使用 global 语句时,Python 会在全局作用域中查找该变量。

阅读:为什么变量有值时我会收到一个UnboundLocalError?

nonlocal:对于嵌套函数,您可以在py3.x中使用 nonlocal 语句修改在封闭函数中声明的变量。


但是类的工作方式不同,类 A 中声明的变量 x 实际上变成了 A.x

>>> x = 'outer'
>>> class A:
...    x += 'inside'  #use the value of global `x` to create a new attribute `A.x`
...    print x        #prints `A.x`
...     
outerinside
>>> print x
outer

您也可以直接从全局范围访问类属性:
>>> A.x
'outerinside'

在类中使用global:

>>> x = 'outer'
>>> class A:
...     global x
...     x += 'inner' #now x is not a class attribute, you just modified the global x
...     print x
...     
outerinner
>>> x
'outerinner'
>>> A.x
AttributeError: class A has no attribute 'x'

在类中,函数的陷阱不会引发错误:

>>> x = 'outer'
>>> class A:
...     print x                      #fetch from globals or builitns
...     x = 'I am a class attribute' #declare a class attribute
...     print x                      #print class attribute, i.e `A.x`
...     
outer
I am a class attribute
>>> x
'outer'
>>> A.x
'I am a class attribute'

LEGB规则:如果没有使用globalnonlocal,那么Python会按照以下顺序进行搜索。

>>> outer = 'global'
>>> def func():
        enclosing = 'enclosing'
        def inner():
                inner = 'inner'
                print inner           #fetch from (L)ocal scope
                print enclosing       #fetch from (E)nclosing scope
                print outer           #fetch from (G)lobal scope
                print any             #fetch from (B)uilt-ins
        inner()
...         
>>> func()
inner
enclosing
global
<built-in function any>

你的 Common Gotcha 为什么会抛出错误,但 OP 的 incr() 函数仅调用 print(x) 却打印全局值 1。 - Max
你引用的文章有很好的解释。在你的例子中,因为对x进行了赋值,所以变量被声明为局部变量。而在 OP 的例子中,x 被定义为 incr() 函数的全局变量。 - Max

5

从Python 作用域和命名空间:

重要的是要意识到,作用域是以文本方式确定的:在模块中定义的函数的全局作用域是该模块的命名空间,无论函数被从何处或通过什么别名调用。另一方面,实际的名称搜索是在运行时动态完成的 - 但是,语言定义正在向静态名称解析发展,即在“编译”时进行,因此不要依赖于动态名称解析! (事实上,局部变量已经被静态确定。)

这意味着,对于x = x + 1,作用域在函数调用之前静态确定。由于这是一个赋值操作,所以'x'成为一个局部变量,而不是全局查找。

这也是为什么在函数中禁止使用from mod import *的原因。因为解释器不会在编译时为您导入模块,以了解您在函数中使用的名称。也就是说,它必须在编译时知道函数中引用的所有名称。


4
这是Python遵循的规则-习惯就好;-) 有一个实际原因:编译器和人类读者可以通过仅查看函数来确定哪些变量是本地变量。哪些名称是本地的与函数出现的上下文无关,遵循限制您必须盯着源代码以回答问题的规则通常是个好主意。
关于:
我假设它这样做是因为它在其本地范围内找不到x,因此搜索其封闭范围并找到x。
不完全正确:编译器在编译时确定哪些名称是本地的。运行时没有动态的“嗯-这是本地的还是全局的?”搜索。精确规则在此处详细说明。
关于为什么不需要声明名称为global才能引用其值,我喜欢Fredrik Lundh在这里的旧答案。实际上,声明global语句确实有助于代码读者意识到函数可能会重新绑定全局名称。

1
因为这就是它被设计成的工作方式。
基本上,如果您在函数中的任何位置有一个赋值语句,那么该变量就会变成该函数的局部变量(除非您当然使用了global)。

3
我理解了这个问题,但为什么函数不能像课堂例子那样工作呢?在课堂示例中,Incr可以使用来自封闭作用域的x值来赋值给一些本地x并将该值增加1。那么为什么函数不能做同样的事情呢? - Shashank
1
他们可能会。但是他们不会,也永远不会;-) 我认为Guido会认为这个类的例子是一个bug,但我没有看到他对此发表评论。 - Tim Peters

1
因为这会导致很难追踪错误!当你键入 x = x + 1 时,你可能意味着要增加封闭作用域中的 x…或者你可能只是忘记了已经在其他地方使用过 x,并试图声明一个局部变量。我更喜欢解释器只允许您更改父命名空间,如果您打算这样做-这样您就不会无意中这样做。

0

我可以尝试并做出一个有根据的猜测,为什么它会这样工作。

当Python在您的函数中遇到字符串x = x + 1时,它必须决定在哪里查找x

它可以说“第一个出现的x是全局的,第二个是本地的”,但这是相当模糊的(因此违反了Python的哲学)。这可能成为语法的一部分,但它潜在地导致棘手的错误。因此,决定保持一致,并将所有出现视为全局或本地变量。

有一个赋值,因此如果x应该是全局的,则会有一个global语句,但没有找到。

因此,x是本地的,但它没有绑定到任何东西,但它在表达式x + 1中使用。抛出UnboundLocalError


我刚刚测试了一下,在类定义中 x = x + 1 的例子是可以工作的。 - Nicole
在他的例子中,我认为有三种处理出现次数的方式:全局变量、局部变量和闭包值。 - Neil G
1
如果这是模棱两可的,因此违反了Python哲学,那么为什么在类中允许它呢?在没有global关键字的情况下,我认为它应该像在类定义中一样将global(x) + 1分配给local(x)。我想我所问的就是,为什么这在类中与函数中的工作方式不同呢? - Shashank
没错,我没有注意到你的类示例(实际上这是我第一次在Python中看到任何类定义中的代码执行,所以我的头脑就忽略了它 :) @Ashwini的答案提供了更多详细信息。 - fjarri

-1
作为一个额外的例子,针对在类中新创建的A.x。在类内部将x重新赋值为“inner”并不会更新全局变量x的值,因为它现在是一个类变量。
x = 'outer'
class A:
    x = x
    print(x)
    x = 'inner'
    print(x)

print(x)
print(A.x)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接