上周五,我去了一次工作面试,不得不回答以下问题:为什么这段代码会引发异常(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
东西,它试图var
从inner
的本地范围获取:
14 LOAD_FAST 0 (var)
17 LOAD_CONST 2 (2)
20 INPLACE_ADD
21 STORE_FAST 0 (var)
当然,我们会得到一个错误。现在, 这是我没有得到的
:为什么我们不能使用来检索,然后var
用来LOAD_DEREF
将其存储在inner
范围内STORE_FAST
?我的意思是,这对于“内部作用域”赋值似乎是可以的,同时在某种程度上更直观。至少+=
代码可以完成我们想要的工作,而我无法提出一种情况,其中所描述的方法可能会使事情变得混乱。
你能?我觉得我在这里错过了一些东西。
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
,任何中级Pythonhtml" target="_blank">程序员都知道如何在脑海中遍历该规则并找出出了什么问题。
同时,请注意,当在现实生活中的代码中出现这种情况时,有很简单的方法可以明确地解决它。例如:
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