Python class 范围规则

Python class scoping rules

编辑: 看起来这是一个非常古老的 "bug" 或者,实际上是一个功能。参见,例如 this mail

我正在尝试了解 Python 范围规则。更准确地说,我认为我了解它们,但后来我发现了这段代码 here:

x = "xtop"
y = "ytop"
def func():
    x = "xlocal"
    y = "ylocal"
    class C:
        print(x)
        print(y)
        y = 1
func()

在Python3.4中输出为:

xlocal
ytop

如果我用一个函数替换内部 class 那么它合理地给出 UnboundLocalError。你能解释一下为什么它在 classes 中表现出这种奇怪的方式吗?选择这种范围规则的原因是什么?

首先关注闭包的情况——函数中的函数:

x = "xtop"
y = "ytop"
def func():
    x = "xlocal"
    y = "ylocal"
    def inner():
 #       global y
        print(x)
        print(y)
        y='inner y'
        print(y)
    inner()  

注意 inner 中注释掉的 global 如果你 运行 这个,它会复制你得到的 UnboundLocalError。为什么?

运行dis.dis就可以了:

>>> import dis
>>> dis.dis(func)
  6           0 LOAD_CONST               1 ('xlocal')
              3 STORE_DEREF              0 (x)

  7           6 LOAD_CONST               2 ('ylocal')
              9 STORE_FAST               0 (y)

  8          12 LOAD_CLOSURE             0 (x)
             15 BUILD_TUPLE              1
             18 LOAD_CONST               3 (<code object inner at 0x101500270, file "Untitled 3.py", line 8>)
             21 LOAD_CONST               4 ('func.<locals>.inner')
             24 MAKE_CLOSURE             0
             27 STORE_FAST               1 (inner)

 14          30 LOAD_FAST                1 (inner)
             33 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
             36 POP_TOP
             37 LOAD_CONST               0 (None)
             40 RETURN_VALUE

请注意 xfunc 内部的 y 的不同访问模式。在 inner 中使用 y='inner y' 创建了 UnboundLocalError

现在取消注释 inner 中的 global y。现在您已明确创建 y 成为全球顶级版本,直到辞去 y='inner y'

取消注释 global,打印:

xlocal
ytop
inner y

可以通过以下方式获得更合理的结果:

x = "xtop"
y = "ytop"
def func():
    global y, x
    print(x,y)
    x = "xlocal"
    y = "ylocal"
    def inner():
        global y
        print(x,y)
        y = 'inner y'
        print(x,y)
    inner()    

打印:

xtop ytop
xlocal ylocal
xlocal inner y

闭包 class 的分析因实例与 class 变量以及什么/何时执行裸体 class(没有实例)而变得复杂。

底线是相同的:如果您在本地命名空间之外引用一个名称,然后在本地分配给相同的名称,您会得到一个令人惊讶的结果。

与'fix'相同:使用全局关键字:

x = "xtop"
y = "ytop"
def func():
    global x, y
    x = "xlocal"
    y = "ylocal"
    class Inner:
        print(x, y)
        y = 'Inner_y'
        print(x, y) 

打印:

xlocal ylocal
xlocal Inner_y

您可以在 PEP 3104

中阅读有关 Python 3 个范围规则的更多信息

TL;DR:此行为自 Python 2.1 PEP 227: Nested Scopes 以来就存在,并且在当时就已为人所知。如果在 class 主体中分配了一个名称(如 y),则假定它是一个 local/global 变量;如果它没有分配给 (x),那么它也可能指向一个闭包单元。词法变量不会显示为 local/global 名称到 class 正文。


在 Python 3.4 上,dis.dis(func) 显示以下内容:

>>> dis.dis(func)
  4           0 LOAD_CONST               1 ('xlocal')
              3 STORE_DEREF              0 (x)

  5           6 LOAD_CONST               2 ('ylocal')
              9 STORE_FAST               0 (y)

  6          12 LOAD_BUILD_CLASS
             13 LOAD_CLOSURE             0 (x)
             16 BUILD_TUPLE              1
             19 LOAD_CONST               3 (<code object C at 0x7f083c9bbf60, file "test.py", line 6>)
             22 LOAD_CONST               4 ('C')
             25 MAKE_CLOSURE             0
             28 LOAD_CONST               4 ('C')
             31 CALL_FUNCTION            2 (2 positional, 0 keyword pair)
             34 STORE_FAST               1 (C)
             37 LOAD_CONST               0 (None)
             40 RETURN_VALUE

LOAD_BUILD_CLASSbuiltins.__build_class__ 加载到堆栈上;这是用参数 __build_class__(func, name) 调用的;其中 func 是 class 正文,name'C'。 class 主体是函数 func:

的常量#3
>>> dis.dis(func.__code__.co_consts[3])
  6           0 LOAD_NAME                0 (__name__)
              3 STORE_NAME               1 (__module__)
              6 LOAD_CONST               0 ('func.<locals>.C')
              9 STORE_NAME               2 (__qualname__)

  7          12 LOAD_NAME                3 (print)
             15 LOAD_CLASSDEREF          0 (x)
             18 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             21 POP_TOP

  8          22 LOAD_NAME                3 (print)
             25 LOAD_NAME                4 (y)
             28 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             31 POP_TOP

  9          32 LOAD_CONST               1 (1)
             35 STORE_NAME               4 (y)
             38 LOAD_CONST               2 (None)
             41 RETURN_VALUE

在 class 主体中,x 通过 LOAD_CLASSDEREF (15) 访问,而 y 通过 LOAD_NAME (25) 加载。 LOAD_CLASSDEREF 是一个 Python 3.4+ 操作码,用于从闭包单元格中加载值,特别是在 class 主体内(在以前的版本中,使用了通用的 LOAD_DEREF); LOAD_NAME 用于从 locals 加载值,然后是 globals。然而,闭包单元既不显示为局部变量也不显示为全局变量。

现在,因为名称 y 存储在 class 正文 (35) 中,所以它一直被用作 local/global 名称而不是闭包单元。 闭包单元不会显示为 class 主体的局部变量。

这种行为是真实的 ever since implementing PEP 227 - nested scopes。当时 BDFL 表示这不应该被修复 - 因此这已经持续了 13 年以上。


自 PEP 227 以来的唯一变化是在 Python 3 中添加了 nonlocal;如果在 class 正文中使用它,则 class 正文可以设置包含范围内的单元格的值:

x = "xtop"
y = "ytop"
def func():
    x = "xlocal"
    y = "ylocal"
    class C:
        nonlocal y  # y here now refers to the outer variable
        print(x)
        print(y)
        y = 1

    print(y)
    print(C.y)

func()

现在的输出是

xlocal
ylocal
1
Traceback (most recent call last):
  File "test.py", line 15, in <module>
    func()
  File "test.py", line 13, in func
    print(C.y)
AttributeError: type object 'C' has no attribute 'y'

print(y)读取包含范围的单元格y的值,y = 1在该单元格中设置值;在这种情况下,没有为 class C.

创建属性