在Python中,即使未被执行,if条件语句中的变量也会隐藏全局作用域。

11
def do_something():
    print 'doing something...'

def maybe_do_it(hesitant=False):
    if hesitant:
        do_something = lambda: 'did nothing'
    result = do_something()
    print result

maybe_do_it()

这段代码的结果是:

  File "scope_test.py", line 10, in <module>
    maybe_do_it()
  File "scope_test.py", line 7, in maybe_do_it
    result = do_something()
UnboundLocalError: local variable 'do_something' referenced before assignment

但是这段代码按预期会打印出 "did something...":

def do_something():
    print 'doing something...'

def maybe_do_it(hesitant=False):
    result = do_something()
    print result

maybe_do_it()

尽管 if 语句内的条件从来没有被执行,这个函数怎么会被覆盖?这种情况在 Python 2.7 中发生了 —— 在 Python 3 中也一样吗?


1
请看这里:https://dev59.com/Q3RC5IYBdhLWcg3wOeWB 引用:“Python在函数中处理变量的方式取决于您是否从函数内部为其分配值。” - flornquake
@flornquake 是的,但是我什么时候给那个变量赋值了呢?那个变量是在从未执行的代码中被赋值的吗? - Buttons840
2
@Buttons840,如果您在函数中有任何分配给某个变量的任务,则局部变量将遮盖全局变量。 - zch
4个回答

8
尽管if语句内的条件从未执行,但是该函数如何被覆盖?
在编译时决定变量是本地还是全局的。如果函数中有任何变量赋值,它都是本地变量,无论是否执行该赋值。
这在Python 2.7中发生 - 在Python 3中也是如此。
顺便说一句,在Python 2中,您可以通过使用exec(不建议)来覆盖此行为:
def do_something():
    print 'doing something...'

def maybe_do_it(hesitant=False):
    if hesitant:
        exec "do_something = lambda: 'did nothing'"
    result = do_something()
    print result

maybe_do_it(False)    # doing something...
maybe_do_it(True)    # did nothing

函数内部的exec会在执行时推迟决定是在全局还是局部查找变量。


5
为什么这个回答会被踩?也许是因为它花了太多时间在笨拙的“exec”解决方法上,但它明确表示不推荐使用,并且只有在正确解释发生了什么之后才会涉及到这一点。 - abarnert
1
+1;并不是因为我认为这是解决问题的好方法,而是一个糟糕的方法;但是因为我学到了东西。在这里使用exec会导致最终返回被编译为“LOAD_NAME”而不是“LOAD_GLOBAL”。 - SingleNegationElimination

6
根据Python Execution Model文档的说明:
如果在代码块中发生名称绑定操作,则块内所有使用该名称的地方都被视为对当前块的引用。当在绑定名称之前在块内部使用名称时,这可能会导致错误。这个规则很微妙。Python缺乏声明,并允许名称绑定操作在代码块中的任何位置发生。可以通过扫描整个块的文本以查找名称绑定操作来确定代码块的局部变量。
这是语言的规则。就是这样。 :D

3
这个答案的好处在于,即使Python实现方式不像CPython那样编译成字节码,也会发生完全相同的事情。但我认为其他两个回答中关于编译的解释有助于理解问题,只要记住它们只是在描述CPython。 - abarnert

1
当Python编译成字节码(生成*.pyc文件)时,由于在函数do_something中有一行do_something = lambda: 'did nothing',因此即使控制流没有将解释器带到该行,do_something现在被视为局部变量。
这是出人意料的主要原因是:
  1. 与常见信仰相反,Python是编译语言

  2. 这很不直观。

基本上,我认为只有在实现糟糕的设计时才会出现这个问题。当您在函数内重新分配do_something时,您正在操作全局作用域-这很少是一个好主意。
*正如已经指出的那样,这实际上不仅适用于编译为字节码的Python(CPython)-它实际上是该语言的一个特性。我的解释细节(以字节码的术语表达)仅涉及CPython。

1
我的世界观被动摇了... :( - Buttons840
振作起来!事情并没有那么糟糕。 - Mike Vella

0

是的,Python 3 中也是一样的。在大多数情况下,这是可取的,尽管不完全直观。也许你必须是荷兰人。许多人可能熟悉提升(由 JavaScript 推广?)。它也发生在 Python 中,只是 Python 不是使用 undefined 值,而是引发一个 UnboundLocalError。比较:

> // JavaScript example
> var x = 1;
> function foo() {
    if (!x) { // x is declared locally below, so locally x is undefined
      var x = 2;
    }
    return x;
  }
> foo();
2

>>> # Everything else is Python 3
>>> x = 1
>>> def foo():
...   if not x: # x is bound below, which constitutes declaring it as a local
...     x = 2
...   return x
... 
>>> foo()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in foo
UnboundLocalError: local variable 'x' referenced before assignment

到目前为止,Python一直很稳定,但有一个(令人不安的)解决方法:
>>> def foo():
...   if not 'x' in locals():
...     x = 2
...   return x
... 
>>> foo()
2

这很有效,我们已经被告知

通过扫描整个代码块的文本以查找名称绑定操作,可以确定代码块的局部变量。

但是,locals()难道不会给我们所有的局部名称吗?显然不是。实际上,尽管前面的声明,Python暗示局部符号表可以在locals()内置函数的描述中发生更改:

更新并返回表示当前[我强调]局部符号表的字典。

我曾经认为“当前”一词指的是值,现在我认为它也指键。但最终我认为这意味着没有办法(除了转储和解析帧的源代码之外)枚举所有在本地声明的名称(这并不是说您不能使用try / except UnboundLocalError来确定特定名称是否为本地名称。)

def foo():
    # Some code, including bindings
    del x, y, z # or any other local names
    # From this point, is it programmatically knowable what names are local?

我认为这是隐式声明语言和显式声明语言之间的根本区别。


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