Python:LOAD_FAST 与 LOAD_DEREF 就地添加
问题: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
更多推荐

所有评论(0)