【问题标题】:Why can functions in Python print variables in enclosing scope but cannot use them in assignment?为什么 Python 中的函数可以在封闭范围内打印变量但不能在赋值中使用它们?
【发布时间】:2013-09-22 17:30:17
【问题描述】:

如果我运行以下代码:

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

这在回溯中给了我以下错误:

UnboundLocalError:赋值前引用了局部变量“x”。

当 Python 无法像我的 class Incr 那样找到用于赋值的 x 值时,为什么 Python 不只是在封闭空间中搜索 x?请注意,我不是在问如何使此功能起作用。我知道如果我执行以下操作,该功能将起作用:

x = 1

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

incr()

这将正确打印:

1
2

正如我所料。我要问的是,当关键字global 不存在时,为什么它不只是从封闭范围中拉出x,就像我在上面的课程中所做的那样。当解释器清楚地知道某些x 存在时,为什么解释器觉得有必要将其报告为UnboundLocalError。由于该函数能够读取x 处的值进行打印,我知道它具有x 作为其封闭范围的一部分......那么为什么这不像类示例那样工作?

为什么使用x 的值进行打印与使用其值进行赋值如此不同?我就是不明白。

【问题讨论】:

  • 这是 Python2 还是 Python3?
  • 你考虑过在incr()函数中使用x+=1吗? __add____iadd__ 之间可能存在差异
  • @Ethan 这是 Python 2.75。我使用括号是因为我最近经常切换语言,而且它更加统一。
  • @inspectorG4dget 如果您尝试x+=1,则会出现相同的结果。

标签: python scope


【解决方案1】:

类和函数是不同的,类内部的变量实际上是作为类的属性分配给类的命名空间的,而函数内部的变量只是普通的变量,不能在外部访问。

函数内部的局部变量实际上是在第一次解析函数时决定的,python不会在全局范围内搜索它们,因为它知道你将其声明为局部变量。

因此,一旦 python 看到 x = x + 1(assignment) 并且没有为该变量声明 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 for 在global 范围内查找该变量。

阅读:Why am I getting an UnboundLocalError when the variable has a value?

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>

【讨论】:

  • 这个答案应该有更多的支持。尽管我已经使用这些功能很长时间了,但我还是从这篇文章中学到了很多关于 Python 的知识。
  • 为什么你的 Common Gotcha 会抛出一个错误,但是 OP 的 incr() 函数只调用 print(x) 打印全局值 1。
  • 你引用的文章有很好的解释。在您的示例中,由于对 x 进行了赋值,因此该变量被声明为本地变量。而在 OP 的示例中,x 被定义为函数 incr() 的全局变量。
【解决方案2】:

来自 Python scopes and namespaces:

重要的是要认识到范围是由文本确定的:模块中定义的函数的全局范围是该模块的命名空间,无论该函数是从何处调用或通过什么别名调用的。另一方面,名称的实际搜索是在运行时动态完成的——然而,语言定义正在向静态名称解析发展,在“编译”时,所以不要依赖动态名称解析! (其实局部变量已经是静态确定的了。)

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

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

【讨论】:

    【解决方案3】:

    这是 Python 遵循的规则——习惯它;-) 有一个实际的原因:编译器和人类读者都可以通过查看函数来确定哪些变量是本地的。哪些名称是本地名称与函数出现的上下文无关,遵循限制回答问题时必须盯着的源代码数量的规则通常是一个非常好的主意。

    关于:

    我认为它这样做是因为它在本地范围内找不到 x,所以 它搜索其封闭范围并找到 x。

    不完全是:编译器确定在编译时哪些名称是本地名称,哪些不是本地名称。没有动态的“嗯——这是本地的还是全球的?”搜索在运行时进行。 precise rules are spelled out here

    至于为什么你不需要声明一个名字global来引用它的值,我喜欢Fredrik Lundh's old answer here。在实践中,global 语句提醒代码读者一个函数可能正在重新绑定一个全局名称确实很有价值。

    【讨论】:

      【解决方案4】:

      因为这就是它的设计方式。

      基本上,如果您在函数中anywhere 有一个赋值,那么该变量将成为该函数的本地变量(当然,除非您使用过global)。

      【讨论】:

      • 好的,我明白了,但是为什么不能像类示例一样工作呢?在类示例中,Incr 能够使用封闭范围中的 x 值来分配给某个局部 x 并将该值增加 1。那么为什么函数不能这样做呢?
      • 他们可以。但他们不会也永远不会;-) 我希望 Guido 会认为类示例是一个错误,但我没有看到他对此发表评论。
      【解决方案5】:

      因为它会导致非常难以追踪错误!

      当您键入 x = x + 1 时,您可能打算在封闭范围内增加一个 x...或者您可能只是忘记了您已经在其他地方使用了 x 并试图声明一个局部变量。

      我希望解释器只允许您更改父命名空间,如果您打算这样做 - 这样您就不会意外地这样做。

      【讨论】:

        【解决方案6】:

        我可以尝试并有根据地猜测它为什么会这样工作。

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

        它可以说“x 的第一个出现是全局的,第二个是本地的”,但这是非常模棱两可的(因此违反了 Python 哲学)。这可以作为语法的一部分,但它可能会导致棘手的错误。因此,决定对此保持一致,并将所有出现的事件视为全局变量或局部变量。

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

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

        【讨论】:

        • 我刚刚测试过,x = x + 1 示例在类定义中有效。
        • 在他的示例中,我认为有 三种 方式来处理事件:全局变量、局部变量和封闭值。
        • 如果它是模棱两可的,因此违反了 Python 哲学,那么为什么在类中允许它呢?在没有 global 关键字的情况下,我认为它应该像在类定义中那样简单地将 global(x) + 1 分配给 local(x) 。我想我要问的是,为什么这对类和函数的工作方式不同
        • 是的,我没有注意到您的类示例(这实际上是我第一次在 Python 中的类定义中看到任何代码执行,所以我的想法只是忽略了它:) @Ashwini 的回答提供了更多详细信息关于这个。
        【解决方案7】:

        作为在类中创建的新创建的 A.x 的额外示例。在类中将 x 重新分配给 'inner' 不会更新 x 的全局值,因为它现在是一个类变量。

        x = 'outer'
        class A:
            x = x
            print(x)
            x = 'inner'
            print(x)
        
        print(x)
        print(A.x)
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2017-04-21
          • 1970-01-01
          • 1970-01-01
          • 2016-04-08
          • 2011-11-24
          • 1970-01-01
          • 2022-07-01
          相关资源
          最近更新 更多