python 中奇怪的作用域行为

Weird scoping behavior in python

考虑以下 python 代码片段:

x = 1
class Foo:
    x = 2
    def foo():
        x = 3
        class Foo:
            print(x) # prints 3
Foo.foo()

正如预期的那样,这会打印 3。 但是,如果我们在上面的代码片段中添加一行,行为就会改变:

x = 1
class Foo:
    x = 2
    def foo():
        x = 3
        class Foo:
            x += 10
            print(x) # prints 11
Foo.foo()

并且,如果我们在上面的示例中调换两行的顺序,结果又会发生变化:

x = 1
class Foo:
    x = 2
    def foo():
        x = 3
        class Foo:
            print(x) # prints 1
            x += 10
Foo.foo()

我想了解为什么会发生这种情况,更一般地说,了解导致这种行为的范围规则。根据 LEGB 范围规则,我希望两个片段都打印 3、13 和 3,因为在封闭函数 foo().

中定义了一个 x

Class 块范围是特殊的。记录在案 here:

A class definition is an executable statement that may use and define names. These references follow the normal rules for name resolution with an exception that unbound local variables are looked up in the global namespace. The namespace of the class definition becomes the attribute dictionary of the class. The scope of names defined in a class block is limited to the class block; it does not extend to the code blocks of methods – this includes comprehensions and generator expressions since they are implemented using a function scope.

基本上,class 块不“参与”creating/using 封闭范围。

因此,这实际上是 第一个未按记录工作的示例。我认为这是一个实际的错误。

编辑:

好的,所以实际上,这里有一些来自 data model 的更相关的文档,我认为它实际上与文档一致:

The class body is executed (approximately) as exec(body, globals(), namespace). The key difference from a normal call to exec() is that lexical scoping allows the class body (including any methods) to reference names from the current and outer scopes when the class definition occurs inside a function.

所以 class 块 do 参与 using 封闭范围,但对于自由变量(无论如何都是正常的)。在我引用的第一篇文档中,关于“在全局命名空间中查找未绑定的局部变量”的部分适用于 通常由编译器 标记为局部的变量。因此,考虑这个臭名昭著的错误,例如:

x = 1
def foo():
    x += 1
    print(x)

foo()

会抛出未绑定的本地错误,但等效的 class 定义:

x = 1
class Foo:
    x += 1
    print(x)

将打印 2.

基本上,如果在 class 块中的任何地方有一个赋值语句,它是“局部的”,但如果有一个未绑定的局部区域,它会检查全局范围而不是抛出 UnboundLocal错误。

因此,在您的第一个示例中,它不是局部变量,它只是一个自由变量,并且解析通过正常规则进行。在接下来的两个示例中,您使用了一个赋值语句,将 x 标记为“本地”,因此,如果它在本地命名空间中未绑定,它将在全局命名空间中查找。