如何在实际代码中出现"NameError: 在封闭作用域中引用赋值前的自由变量'var'"?
我在Python聊天室闲逛的时候,有人进来报告了一个异常错误:
NameError: free variable 'var' referenced before assignment in enclosing scope
我之前从没见过这个错误信息,而用户只提供了一小段代码,这段代码本身不可能导致这个错误,所以我开始在网上查资料,结果发现信息不多。在我搜索的过程中,用户说他们的问题是“空格问题”解决了,然后就离开了聊天室。
我试着玩了一下,发现我只能用一些简单的代码重现这个异常,像这样:
def multiplier(n):
def multiply(x):
return x * n
del n
return multiply
这段代码给我的结果是:
>>> triple = multiplier(3)
>>> triple(5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in multiply
NameError: free variable 'n' referenced before assignment in enclosing scope
这看起来没什么问题,但我很难理解这个异常是怎么在真实代码中出现的,因为我上面的例子:
- 非常简单
- 不太可能偶然发生
... 但显然,它确实发生了,正如我在问题开头提到的报告。
那么,这个特定的异常在真实代码中是怎么发生的呢?
3 个回答
我在做一个Flask应用的时候遇到了一个错误信息。
我当时在修复一个关于数据库的错误。我在一个叫做app.py
的文件里写了很多代码来设置这个应用(这些代码不在任何函数里),但后来我在文档里看到,这些设置代码应该放在一个叫create_app()
的函数里。于是我一开始就在文件底部创建了这个create_app()
函数,并把db
实例的创建代码和一些我之前放在if __name__ == '__main__'
条件里的代码一起移动到了这个函数里。
这样做没有成功,所以我又把def create_app()
这一行移动到了文件的顶部,并且几乎把文件里的所有代码都缩进了(其中包括一些事件处理程序)。这样一来,db
对象的创建就出现在了它在一些事件处理程序中被使用的下面,这就导致了这个错误。
我通过把db
对象的创建代码移回到create_app()
函数的顶部,放在它在事件处理程序中被使用之前,解决了这个错误。
虽然我来回答这个问题有点晚,但我觉得我可以提供一些详细的信息,帮助未来的读者理解这个情况。
错误信息是:
NameError: 自由变量 'var' 在外部作用域中被引用,但在赋值之前
当我们提到自由变量时,其实是在讨论嵌套函数。Python 通过一些“魔法”让嵌套函数能够访问它们父函数中定义的变量。如果我们有:
def outer():
foo = 10
def inner():
print(foo)
return inner
outer()() # 10
通常情况下,inner
函数是无法访问 foo
的。为什么呢?因为在调用并执行 outer
函数的代码后,它的命名空间就被销毁了。简单来说,函数内部定义的任何局部变量在函数结束后都不再可用。
但是我们可以访问...
这种“魔法”是通过 “单元对象” 实现的:
“单元”对象用于实现被多个作用域引用的变量。对于每个这样的变量,都会创建一个单元对象来存储它的值;每个引用该值的栈帧的局部变量都包含对外部作用域中使用该变量的单元的引用。当访问这个值时,使用的是单元中包含的值,而不是单元对象本身。
为了查看单元中隐藏的存储值(稍后我们会讨论 __closure__
):
def outer():
foo = 10
def inner():
print(foo)
return inner
print(outer().__closure__[0].cell_contents) # 10
它是如何工作的?
在 “编译” 时,
当 Python 看到一个函数在另一个函数内部时,它会记录下嵌套函数中引用的变量名,这些变量实际上是在外部函数中定义的。这些信息会存储在两个函数的代码对象中:外部函数的 co_cellvars
和内部函数的 co_freevars
:
def outer():
foo = 10
def inner():
print(foo)
return inner
print(outer.__code__.co_cellvars) # ('foo',)
print(outer().__code__.co_freevars) # ('foo',)
现在是执行时...(见代码)
当 Python 想要执行 outer
函数时,它会为每个记录下的变量(co_cellvars
)创建一个“单元对象”。
然后在执行代码时,每当看到对这些变量的赋值时,它就会把对应的单元对象填充上这个变量的值。(记住,它们间接包含了实际的值。)
当执行到创建内部函数的那一行时,Python 会把所有创建的单元对象放在一个元组中。这个元组随后被赋值给内部函数的 __closure__
。
关键是,当这个元组被创建时,有些单元可能还没有值。它们是 空的(见输出)!...
在这个时候,当你调用内部函数时,那些没有值的单元会引发之前提到的错误!
def outer():
foo = 10
def inner():
print(foo)
try:
print(boo)
except NameError as e:
print(e)
# Take a look at inner's __closure__ cells
print(inner.__closure__)
# So one boo is empty! This raises error
inner()
# Now lets look at inner's __closure__ cells one more time (they're filled now)
boo = 20
print(inner.__closure__)
# This works fine now
inner()
outer()
来自 Python 3.10 的输出:
(<cell at 0x7f14a5b62710: empty>, <cell at 0x7f14a5b62830: int object at 0x7f14a6f00210>)
10
free variable 'boo' referenced before assignment in enclosing scope
(<cell at 0x7f14a5b62710: int object at 0x7f14a6f00350>, <cell at 0x7f14a5b62830: int object at 0x7f14a6f00210>)
10
20
错误 自由变量 'boo' 在外部作用域中被引用,但在赋值之前
现在是可以理解的了。
注意:在 Python 3.11 中,这个错误的表述被改成了:
cannot access free variable 'boo' where it is not associated with a value in enclosing scope
但其核心思想是一样的。
如果你查看 outer
函数的字节码,你会看到我在“执行时”部分提到的步骤是如何运作的:
from dis import dis
def outer():
foo = 10
def inner():
print(foo)
print(boo)
boo = 20
return inner
dis(outer)
来自 Python 3.11 的输出:
0 MAKE_CELL 1 (boo)
2 MAKE_CELL 2 (foo)
3 4 RESUME 0
4 6 LOAD_CONST 1 (10)
8 STORE_DEREF 2 (foo)
5 10 LOAD_CLOSURE 1 (boo)
12 LOAD_CLOSURE 2 (foo)
14 BUILD_TUPLE 2
16 LOAD_CONST 2 (<code object inner at 0x7fb6d4731a30, file "", line 5>)
18 MAKE_FUNCTION 8 (closure)
20 STORE_FAST 0 (inner)
8 22 LOAD_CONST 3 (20)
24 STORE_DEREF 1 (boo)
9 26 LOAD_FAST 0 (inner)
28 RETURN_VALUE
MAKE_CELL
是 Python 3.11 中的新内容。
STORE_DEREF
将值存储在单元对象中。
想象一下一个更复杂的函数,其中的 n
根据某些条件被绑定,或者不被绑定。你不需要去用 del
删除那个名字,编译器在看到赋值的时候也会这样处理,所以这个名字是局部的,但如果代码路径没有被执行到,这个名字就永远不会被赋值任何东西。再举个简单的例子:
def f():
def g(x):
return x * n
if False:
n = 10
return g