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
不能是本地的,因为它在赋值之前被取消引用,并且它不能是非本地的(除非声明为 global
或 nonlocal
),因为它正在被分配给。
语言是这样设计的,因此 (a) 您不会不小心踩到全局变量:分配给变量会使它成为局部变量,除非您显式声明它 global
或 nonlocal
。 (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 += 1
是 LOAD_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
上周五我去参加工作面试,不得不回答以下问题:为什么这段代码会引发异常(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
不能是本地的,因为它在赋值之前被取消引用,并且它不能是非本地的(除非声明为 global
或 nonlocal
),因为它正在被分配给。
语言是这样设计的,因此 (a) 您不会不小心踩到全局变量:分配给变量会使它成为局部变量,除非您显式声明它 global
或 nonlocal
。 (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 += 1
是 LOAD_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