如何在实际代码中出现"在封闭范围内赋值之前引用的NameError:自由变量'var'?

Zer*_*eus 6 python closures scope exception free-variable

当我在Python聊天室闲逛时,有人进入并报告了以下异常:

NameError: free variable 'var' referenced before assignment in enclosing scope
Run Code Online (Sandbox Code Playgroud)

我之前从未见过这个错误消息,并且用户只提供了一个不能自己引起错误的小代码片段,所以关闭我去google搜索信息,并且...似乎没有多少.在我搜索时,用户报告他们的问题已解决为"空白问题",然后离开了房间.

玩了一下后,我只能用这样的玩具代码重现异常:

def multiplier(n):
    def multiply(x):
        return x * n
    del n
    return multiply
Run Code Online (Sandbox Code Playgroud)

这给了我:

>>> 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
Run Code Online (Sandbox Code Playgroud)

一切都很好,但我很难弄清楚这种异常如何在野外发生,因为上面我的例子是

  1. 很傻
  2. 不太可能偶然发生

......但鉴于我在本问题开头提到的报告,显然确实如此.

所以-怎么可以这个特定的例外发生在实际的代码?

Bla*_*ack 5

想想一个更复杂的功能,n它取决于某些条件,或者不是.您没有del相关名称,如果编译器看到一个赋值,也会发生这种情况,因此名称是本地的,但不会采用代码路径,也不会为名称分配任何内容.另一个愚蠢的例子:

def f():
    def g(x):
        return x * n
    if False:
        n = 10
    return g
Run Code Online (Sandbox Code Playgroud)


Sor*_*ary 5

回答得太晚了,但我想我可以就这种情况提供一些详细信息。这将帮助未来的读者了解这里发生了什么。

\n

所以错误消息显示:

\n
\n

NameError:在封闭范围内赋值之前引用了自由变量“var”

\n
\n

当我们谈论自由变量时,我们正在处理嵌套函数。Python 做了一些“魔法”,以便让嵌套函数能够访问其父作用域内定义的变量。如果我们有:

\n
def outer():\n    foo = 10\n    def inner():\n        print(foo)\n    return inner\n\nouter()()  # 10\n
Run Code Online (Sandbox Code Playgroud)\n

通常我们不应该访问fooininner函数。为什么 ?因为在调用并执行函数体之后outer,它的命名空间就被破坏了。基本上,函数内定义的任何局部变量在函数终止后都不再可用。

\n

但我们可以访问...

\n

这个魔法是在“细胞对象的帮助下发生的

\n
\n

\xe2\x80\x9cCell\xe2\x80\x9d 对象用于实现多个作用域引用的变量。对于每个这样的变量,都会创建一个单元对象来存储值;引用该值的每个堆栈帧的局部变量包含对也使用该变量的外部作用域的单元格的引用。当访问该值时,将使用单元格中包含的值而不是单元格对象本身。

\n
\n

__closure__只是为了查看单元格中隐藏的存储值(我们稍后会讨论):

\n
def outer():\n    foo = 10\n    def inner():\n        print(foo)\n    return inner\n\nprint(outer().__closure__[0].cell_contents)  # 10\n
Run Code Online (Sandbox Code Playgroud)\n

它是如何工作的?

\n

“编译”时,

\n

当Python在另一个函数中看到一个函数时,它会记下嵌套函数中引用的变量的名称,这些变量实际上是在外部函数中定义的。此信息存储在两个函数的代码对象中。co_cellvars对于外部函数和co_freevars内部函数:

\n
def outer():\n    foo = 10\n    def inner():\n        print(foo)\n    return inner\n\nprint(outer.__code__.co_cellvars)   # (\'foo\',)\nprint(outer().__code__.co_freevars) # (\'foo\',)\n
Run Code Online (Sandbox Code Playgroud)\n

现在执行时间...,(参见代码)

\n

当Python想要执行该outer函数时,它会为每个变量创建一个“单元对象”(co_cellvars它记下的每个变量()创建一个“单元对象”。

\n

然后,当它遍历这些行时,每当它看到对此类变量的赋值时,它就会用该变量填充相应的单元格对象。(请记住,“它们”间接包含实际值。)

\n

当执行到创建内部函数的行时,Python 会获取所有创建的单元对象并从中创建一个元组。然后将该元组分配给内部函数__closure__

\n

关键是当创建这个元组时,某些单元格可能还没有值。他们是空的(参见输出)!...

\n

此时,当您调用内部函数时,那些没有值的单元格将引发上述错误!

\n
def outer():\n    foo = 10\n\n    def inner():\n        print(foo)\n        try:\n            print(boo)\n        except NameError as e:\n            print(e)\n\n    # Take a look at inner\'s __closure__ cells\n    print(inner.__closure__)\n    # So one boo is empty! This raises error\n    inner()\n\n    # Now lets look at inner\'s __closure__ cells one more time (they\'re filled now)\n    boo = 20\n    print(inner.__closure__)\n    # This works fine now\n    inner()\n\nouter()\n
Run Code Online (Sandbox Code Playgroud)\n

Python 3.10 的输出:

\n
(<cell at 0x7f14a5b62710: empty>, <cell at 0x7f14a5b62830: int object at 0x7f14a6f00210>)\n10\nfree variable \'boo\' referenced before assignment in enclosing scope\n(<cell at 0x7f14a5b62710: int object at 0x7f14a6f00350>, <cell at 0x7f14a5b62830: int object at 0x7f14a6f00210>)\n10\n20\n
Run Code Online (Sandbox Code Playgroud)\n

错误free variable \'boo\' referenced before assignment in enclosing scope现在有意义了。

\n

笔记:此错误在 Python 3.11 中被改写为:

\n
cannot access free variable \'boo\' where it is not associated with a value in enclosing scope\n
Run Code Online (Sandbox Code Playgroud)\n

但想法是一样的。

\n
\n

如果你看一下字节码outer函数的字节码,您会看到我在“执行时间”部分中提到的步骤:

\n
from dis import dis\n\ndef outer():\n    foo = 10\n    def inner():\n        print(foo)\n        print(boo)\n    boo = 20\n    return inner\n\ndis(outer)\n
Run Code Online (Sandbox Code Playgroud)\n

Python 3.11 的输出:

\n
              0 MAKE_CELL                1 (boo)\n              2 MAKE_CELL                2 (foo)\n\n  3           4 RESUME                   0\n\n  4           6 LOAD_CONST               1 (10)\n              8 STORE_DEREF              2 (foo)\n\n  5          10 LOAD_CLOSURE             1 (boo)\n             12 LOAD_CLOSURE             2 (foo)\n             14 BUILD_TUPLE              2\n             16 LOAD_CONST               2 (<code object inner at 0x7fb6d4731a30, file "", line 5>)\n             18 MAKE_FUNCTION            8 (closure)\n             20 STORE_FAST               0 (inner)\n\n  8          22 LOAD_CONST               3 (20)\n             24 STORE_DEREF              1 (boo)\n\n  9          26 LOAD_FAST                0 (inner)\n             28 RETURN_VALUE\n\n
Run Code Online (Sandbox Code Playgroud)\n

MAKE_CELL是Python3.11中的新增内容。
\nSTORE_DEREF将值存储在单元格对象内。

\n