为啥这段代码会报错?
def C():
class C:
C
C()
按照我的理解, 执行 class C 的定义体时, 是可以解析到全局作用域中的函数名 C
的.
我在 Python 社区的论坛也问了一遍, 有更具体的描述:
(不过有点惊讶, 那边似乎比 Emacs China 要冷清得多…
为啥这段代码会报错?
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 字节码反汇编器