Python:LOAD_FAST与LOAD_DEREF,内置添加

oop*_*ode 8 python closures bytecode

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

def outer():
    var = 1

    def inner():
        var += 1
        return var

    return inner
Run Code Online (Sandbox Code Playgroud)

我无法给出正确答案; 这个事实让我心烦意乱,当我回到家时,我很难找到合适的答案.好吧,我已经找到了答案,但现在有别的东西,让我困惑.

我必须事先说,我的问题更多的是关于设计语言时做出的决定,而不是关于它是如何工作的.

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

def outer():
    var = 1

    def inner():
        return var

    return inner
Run Code Online (Sandbox Code Playgroud)

反汇编看起来像这样:

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
Run Code Online (Sandbox Code Playgroud)

当我们尝试将其他内容绑定到var内部函数内部时,这会发生变化:

def outer():
    var = 1

    def inner():
        var = 2
        return var

    return inner
Run Code Online (Sandbox Code Playgroud)

再次拆卸:

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
Run Code Online (Sandbox Code Playgroud)

我们var在本地存储,符合文档中的说法:名称的赋值始终进入最内层范围.

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

14 LOAD_FAST                0 (var)
17 LOAD_CONST               2 (2)
20 INPLACE_ADD
21 STORE_FAST               0 (var)
Run Code Online (Sandbox Code Playgroud)

当然,我们得到一个错误.现在,这是我没有得到的:为什么我们不能var用a来检索LOAD_DEREF,然后将它存储在inner一个范围内STORE_FAST?我的意思是,对于"最里面的范围"分配内容似乎没问题,同时它更直观地需要.至少+=代码可以做我们想做的事情,而且我无法想出所描述的方法可能会搞砸的情况.

你能?我觉得我在这里错过了一些东西.

aba*_*ert 11

Python有一个非常简单的规则,它将范围中的每个名称分配给一个类别:local,enclosing或global/builtin.

(当然,CPython通过使用FAST本地,DEREF闭包单元以及NAME或GLOBAL查找来实现该规则.)


你改变的规则确实对你死的简单情况有意义,但是很容易想出一些不明确的情况(至少对于人类读者来说,如果没有编译器).例如:

def outer():
    var = 1

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

    return inner
Run Code Online (Sandbox Code Playgroud)

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


即使你可以提出一个更复杂的规则,这个规则很简单.除了易于实现(因此易于调试,优化等),人们也很容易理解.当你得到一个UnboundLocalError,任何中级Python程序员都知道如何通过他的头脑中的规则,并弄清楚出了什么问题.


同时,请注意,当实际代码出现时,有很简单的方法可以明确地解决它.例如:

def inner():
    lvar = var + 1
    return lvar
Run Code Online (Sandbox Code Playgroud)

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

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

def inner():
    nonlocal var
    if spam:
        var = 1
    lvar = var + 1
    return lvar
Run Code Online (Sandbox Code Playgroud)

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

def inner(var=var):
    var += 1
    return var
Run Code Online (Sandbox Code Playgroud)