问题 在Python中,if条件中的变量是否隐藏全局范围,即使它们未被执行?


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

但是这段代码按照预期印刷了“做了些什么......”

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中是否相同?


10287
2017-09-05 22:47


起源

看到这个: stackoverflow.com/questions/370357/... 引用:“Python对函数中的变量的处理方式不同,具体取决于你是否从函数中为它们赋值。” - flornquake
@flornquake是的,但是我什么时候为该变量赋值?代码中分配的变量是否从未执行过? - Buttons840
@ Buttons840,如果你对函数中的某个变量有任何赋值,那么局部变量将影响全局。 - zch


答案:


即使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 松散地说,函数内部将推迟决定是全局查找变量还是本地查找执行时间。


8
2017-09-05 23:06



为什么这会被贬低?也许它不应该花太多时间在hacky上 exec 解决方法,但它明确表示不建议这样做,并且只有在正确解释发生的情况后才会进入。 - abarnert
+1;不是因为我认为这是一个解决这个问题的好方法,它是一个糟糕的问题;但因为我学到了一些东西。这里的exec导致最终返回被编译为 LOAD_NAME 代替 LOAD_GLOBAL - SingleNegationElimination
用于回答问题的+1:“它在Python 3中是否相同”。 - Buttons840


正如文件中所述 Python执行模型

如果名称绑定操作发生在代码块中的任何位置,      块中名称的所有使用都被视为引用      到当前块。使用名称时可能会导致错误      在绑定之前的一个街区内。这个规则很微妙。蟒蛇      缺少声明并允许进行名称绑定操作      代码块中的任何位置。代码块的局部变量      可以通过扫描块的整个文本来确定名称      绑定操作。

这是一种语言规则。这就是它的方式。 :d


6
2017-09-05 23:09



这个答案的好处在于它正确地暗示即使在Python实现中也会发生完全相同的事情 不 以CPython的方式编译为字节码。但我认为另外两个答案在编译方面的解释有助于理解事物,只要你记住他们只是在描述CPython。 - abarnert


当python编译为字节码时(使得 *.pyc 文件)*因为有一个 do_something = lambda: 'did nothing' 在你的功能行 do_something 现在被视为局部变量,即使控制流不在那里使用解释器。

这是出乎意料的主要原因是:

  1. 与普遍看法相反, Python已编译

  2. 这是不直观的。

从根本上说,如果你实施糟糕的设计,我认为这只会成为一个问题。当你重新分配 do_something 在你正在玩全局范围的函数中 - 这不是一个好主意。

*正如已经指出的,这实际上并不适用于编译为字节码(CPython)的Python - 它实际上是该语言的一个特性。我的解释细节(用字节码表示)仅指CPython。


1
2017-09-05 22:53



我对世界的理解动摇了...... :( - Buttons840
振作起来!它并没有那么糟糕。 - Mike Vella


是的,它在Python 3中是相同的。在大多数情况下,如果不是完全直观的行为,这是可取的。 也许你必须是荷兰人。许多人可能熟悉吊装(由JavaScript推广?)。它也发生在Python中,除了它没有 undefined 价值,Python只是提出了一个 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?

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


0
2017-09-06 01:48