问题:Python:LOAD_FAST 与 LOAD_DEREF 就地添加

上周五我去参加工作面试,不得不回答以下问题:为什么这段代码会引发异常(包含var += 1的行上的UnboundLocalError: local variable 'var' referenced before assignment)?

def outer():
    var = 1

    def inner():
        var += 1
        return var

    return inner

我无法给出正确的答案;这个事实真的让我很不安,当我回到家时,我真的很努力地想找到一个正确的答案。好吧,我_已经_找到了答案,但现在还有其他事情让我感到困惑。

我必须提前说我的问题更多是关于设计语言时所做的决定,而不是关于它是如何工作的。

所以,考虑一下这段代码。内部函数是一个 python 闭包,var不是outer的本地函数 - 它存储在一个单元格中(然后从一个单元格中检索):

def outer():
    var = 1

    def inner():
        return var

    return inner

反汇编如下所示:

0  LOAD_CONST               1 (1)
3  STORE_DEREF              0 (var)  # not STORE_FAST

6  LOAD_CLOSURE             0 (var)
9  BUILD_TUPLE              1
12 LOAD_CONST               2 (<code object inner at 0x10796c810)
15 LOAD_CONST               3 ('outer.<locals>.inner')
18 MAKE_CLOSURE             0
21 STORE_FAST               0 (inner)

24 LOAD_FAST                0 (inner)
27 RETURN_VALUE

recursing into <code object inner at 0x10796c810:

0  LOAD_DEREF               0 (var)  # same thing
3  RETURN_VALUE

当我们尝试将其他东西绑定到内部函数中的var时,情况会发生变化:

def outer():
    var = 1

    def inner():
        var = 2
        return var

    return inner

再次拆卸:

0  LOAD_CONST               1 (1)
3  STORE_FAST               0 (var)  # this one changed
6  LOAD_CONST               2 (<code object inner at 0x1084a1810)
9  LOAD_CONST               3 ('outer.<locals>.inner')
12 MAKE_FUNCTION            0  # AND not MAKE_CLOSURE
15 STORE_FAST               1 (inner)

18 LOAD_FAST                1 (inner)
21 RETURN_VALUE

recursing into <code object inner at 0x1084a1810:

0  LOAD_CONST               1 (2)
3  STORE_FAST               0 (var)  # 'var' is supposed to be local

6  LOAD_FAST                0 (var)  
9  RETURN_VALUE

我们将var存储在本地,这符合文档中所说的:对名称的分配总是进入最内部的范围

现在,当我们尝试增加var += 1时,会出现一个讨厌的LOAD_FAST,它试图从inner的本地范围中获取var:

14 LOAD_FAST                0 (var)
17 LOAD_CONST               2 (2)
20 INPLACE_ADD
21 STORE_FAST               0 (var)

当然,我们得到一个错误。现在,这是我不明白的:为什么我们不能用LOAD_DEREF检索var,然后用STORE_FAST将其存储在inner的范围内?我的意思是,这似乎没问题。使用“最内层”分配的东西,同时它在某种程度上更直观可取。至少+=代码会做我们希望它做的事情,而且我想不出所描述的方法可能会搞砸的情况。

你能?我觉得我在这里遗漏了一些东西。

解答

Python 有一个非常简单的规则,将作用域中的每个名称都分配给一个类别:本地、封闭或全局/内置。

(当然,CPython 通过使用 FAST 局部变量、DEREF 闭包单元以及 NAME 或 GLOBAL 查找来实现该规则。)


您更改的规则对于您的死简单案例确实有意义,但很容易提出模棱两可的案例(至少对于人类读者来说,如果不是对于编译器)。例如:

def outer():
    var = 1

    def inner():
        if spam:
            var = 1
        var += 1
        return var

    return inner

var += 1是做LOAD_DEREF还是LOAD_FAST?直到我们知道spam在运行时的值,我们才能知道。这意味着我们无法编译函数体。


即使你能想出一个更复杂但有意义的规则,简单的规则也有内在的美德。除了易于实现(因此易于调试、优化等)之外,它还易于理解。当你得到一个UnboundLocalError时,任何中级 Python 程序员都知道如何通过他脑海中的规则并找出问题所在。


同时,请注意,当这出现在实际代码中时,有非常简单的方法可以显式地解决它。例如:

def inner():
    lvar = var + 1
    return lvar

您想加载闭包变量,并分配给局部变量。他们没有理由需要同名。事实上,即使使用新规则,使用相同的名称也会产生误导——它向读者暗示你正在修改闭包变量,而实际上你并没有。所以只要给他们不同的名字,问题就会消失。

这仍然适用于非本地分配:

def inner():
    nonlocal var
    if spam:
        var = 1
    lvar = var + 1
    return lvar

或者,当然,有一些技巧,比如使用参数默认值来创建一个以闭包变量的副本开始的本地:

def inner(var=var):
    var += 1
    return var
Logo

Python社区为您提供最前沿的新闻资讯和知识内容

更多推荐