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)