问个 Python 的名字解析机制有关的问题

为啥这段代码会报错?

def C():
    class C:
        C
C()

按照我的理解, 执行 class C 的定义体时, 是可以解析到全局作用域中的函数名 C 的.

我在 Python 社区的论坛也问了一遍, 有更具体的描述:

(不过有点惊讶, 那边似乎比 Emacs China 要冷清得多…

第三行访问的 C 其实是 class C 这个类名,这个时候 class C 是没有定义完成的。

注意:下面的字节码分析和 debug 是使用 cpython 3.11.3 进行解释的,不同版本的字节码存在差异,例如,cpython 3.12 删除了 LOAD_CLASSDEREF 这条字节码。

换一个更清晰一点的代码如下:

def C():
    class D:
        D

C()

# NameError: cannot access free variable 'D' where it is not associated with a value in enclosing scope

这段代码的符号表和符号表树如下

Symbol Table:  top
+------+--------+----------+-------+------+-------+------------+----------+
| name | global | nonlocal | local | free | param | referenced | imported |
+------+--------+----------+-------+------+-------+------------+----------+
|  C   |  True  |          |  True |      |       |    True    |          |
+------+--------+----------+-------+------+-------+------------+----------+

Symbol Table:  top.C
+------+--------+----------+-------+------+-------+------------+----------+
| name | global | nonlocal | local | free | param | referenced | imported |
+------+--------+----------+-------+------+-------+------------+----------+
|  D   |        |          |  True |      |       |            |          |
+------+--------+----------+-------+------+-------+------------+----------+

Symbol Table:  top.C.D
+------+--------+----------+-------+------+-------+------------+----------+
| name | global | nonlocal | local | free | param | referenced | imported |
+------+--------+----------+-------+------+-------+------------+----------+
|  D   |        |          |       | True |       |    True    |          |
+------+--------+----------+-------+------+-------+------------+----------+

Symbol Table Tree

top
│
└── C
     └── D

生成这个符号表的代码在 符号表可视化

或者也可以考虑从字节码的角度考虑(这块不熟悉,也只是猜测),上面代码的字节码如下。

>>> import dis
>>> def C():
...     class D:
...         D
...
>>> dis.dis(C)
              0 MAKE_CELL                0 (D)

  1           2 RESUME                   0

  2           4 PUSH_NULL
              6 LOAD_BUILD_CLASS
              8 LOAD_CLOSURE             0 (D)
             10 BUILD_TUPLE              1
             12 LOAD_CONST               1 (<code object D at 0x7fc878be5ca0, file "<stdin>", line 2>)
             14 MAKE_FUNCTION            8 (closure)
             16 LOAD_CONST               2 ('D')
             18 PRECALL                  2
             22 CALL                     2
             32 STORE_DEREF              0 (D)
             34 LOAD_CONST               0 (None)
             36 RETURN_VALUE

Disassembly of <code object D at 0x7fc878be5ca0, file "<stdin>", line 2>:
              0 COPY_FREE_VARS           1

  2           2 RESUME                   0
              4 LOAD_NAME                0 (__name__)
              6 STORE_NAME               1 (__module__)
              8 LOAD_CONST               0 ('C.<locals>.D')
             10 STORE_NAME               2 (__qualname__)

  3          12 LOAD_CLASSDEREF          0 (D)
             14 POP_TOP
             16 LOAD_CONST               1 (None)
             18 RETURN_VALUE

其中上面一块是函数 C 的定义,下面一块是类 D 的定义,我们关注这几条字节码

LOAD_BUILD_CLASS # 开始构造类 D
STORE_DEREF # 将 D 作为 fast local 变量保存

LOAD_CLASSDEREF 从 fast local 中加载类 D

按照时序,很明显第三条加载的时候类 D 还没有保存到 fast local 中。

关于这个猜测,可以去 Debug 一下 Python/ceval.c 的代码行 TARGET(LOAD_CLASSDEREF)。在这个块内,可以清晰的看到 从 locals 中加载 name D 失败,然后进入 format_exc_unbound 逻辑,也就是打印最上面的报错 NameError: cannot access free variable 'D' ... 的逻辑。现在不方便截图,所以不贴debug 信息了。

关于字节码的定义见 Python 字节码反汇编器

3 个赞