Python:LOAD_FAST 与 LOAD_DEREF 就地添加

Python: LOAD_FAST vs. LOAD_DEREF with inplace addition

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

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,然后将其存储在 inner 中的范围带有 STORE_FAST?我的意思是,这似乎是O.K。与 "innermost scope" 分配的东西,同时它更直观地可取。至少 += 代码会做我们想要它做的事情,我想不出所描述的方法会搞砸的情况。

可以吗?我觉得我在这里遗漏了一些东西。

你是不是太难了? var 不能是本地的,因为它在赋值之前被取消引用,并且它不能是非本地的(除非声明为 globalnonlocal),因为它正在被分配给。

语言是这样设计的,因此 (a) 您不会不小心踩到全局变量:分配给变量会使它成为局部变量,除非您显式声明它 globalnonlocal。 (b) 您可以轻松地 使用 外部作用域中的变量值。如果您取消引用未在本地定义的名称,它会在封闭范围内查找它。

您的代码必须先解除对变量的引用,然后才能递增它,因此语言规则使变量既是局部变量又是非局部变量——这是矛盾的。结果:如果您将 var 声明为 nonlocal,您的代码只会 运行。

你挖得太深了。这是语言语义的问题,而不是操作码和单元格的问题。 inner 包含对名称 var:

的赋值
def inner():
    var += 1    # here
    return(var)

所以通过Python执行模型,inner有一个名为var的局部变量,所有尝试在[=11]里面读写名字var =] 使用局部变量。虽然 Python 可以设计为如果本地 var 未绑定,它会尝试闭包的 var,但 Python 不是那样设计的。

Python 有一个非常简单的规则,将作用域中的每个名称分配给一个类别:本地、封闭或 global/builtin.

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


您更改后的规则对于您的极其简单的案例确实有意义,但很容易想到它会模棱两可的情况(至少对于人类 reader,如果不是编译器的话)。例如:

def outer():
    var = 1

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

    return inner

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


即使您可以想出一个更复杂的规则,但规则本身就具有简单的优点。除了易于实施(因此易于调试、优化等)之外,它还易于他人理解。当你得到 UnboundLocalError 时,任何中级 Python 程序员都知道如何在头脑中处理规则并找出问题所在。


同时,请注意,当在现实生活中的代码中出现这种情况时,有非常简单的方法可以明确地解决它。例如:

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

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

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

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

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

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