只用5行代码,说明Python也是有编译阶段Python 虽然常说“解释执行”,但内部是有明确的“编译阶段”的,只是这个编译是即时完成(JIT-like 也不是严格 JIT,而是 CPython 的编译执行流程)。
示例代码与报错
img运行时,在第3行print(x)就报错,而不用等到第4行。为何?
# python test.pyTraceback (most recent call last): File "/root/study-python-vis/test.py", line 5, in <module> foo() File "/root/study-python-vis/test.py", line 3, in foo print(x) ^UnboundLocalError: cannot access local variable 'x' where it is not associated with a value
查看反编译字节码
img左边对应源码行号
从上图dis反编译的字节码可看出,第3行的 LOAD_FAST_CHECK 会检查变量初始化,第4行已不检查了。说明这个函数内部,在编译期间对变量 x 进行了统筹处理。
- • LOAD_FAST_CHECK(*var_num*):将对局部变量co_varnames[var_num]的引用压入堆栈,如果局部变量尚未初始化,则引发UnboundLocalError 。Added in version 3.12.
- • LOAD_FAST(var_num):将对局部变量
co_varnames[var_num]的引用压入栈中。*3.12 版本更改:*此操作码现在仅用于保证局部变量已初始化的情况。它不会引发UnboundLocalError。
0 RESUME 0 1 LOAD_CONST 0 (1) STORE_NAME 0 (x) 2 LOAD_CONST 1 (<code object foo at 0x60e3e8094eb0, file "example.py", line 2>) MAKE_FUNCTION STORE_NAME 1 (foo) 5 LOAD_NAME 1 (foo) PUSH_NULL CALL 0 POP_TOP RETURN_CONST 2 (None)foo 2 RESUME 0 3 LOAD_GLOBAL 1 (print + NULL) LOAD_FAST_CHECK 0 (x) CALL 1 POP_TOP 4 LOAD_FAST 0 (x) LOAD_CONST 1 (1) BINARY_OP 13 (+=) STORE_FAST 0 (x) RETURN_CONST 0 (None)
结果看完了,现在来说说两个阶段:编译与执行。
Python 的执行流程:编译 + 执行
Python 的执行并非逐行直接解释源代码,而是有两个主要阶段:
源代码 (.py) --> 编译阶段 --> 字节码 (.pyc) --> 解释器执行
a) 编译阶段(Compile)
- • Python 读取源代码,解析成 抽象语法树(AST)。
- • AST 被转成 code object(字节码)。
- • 确定 作用域(local/global/nonlocal)
- • 编译阶段 不执行代码(除了顶层表达式求值的特殊情况)。
所以,foo() 中 x += 1 就已经在编译阶段把 x 标记为局部变量。
b) 执行阶段(Run/Interpret)
- • CPython 解释器(
ceval.c)逐条执行字节码: - • 如:LOAD_FAST / LOAD_GLOBAL / BINARY_OP / CALL 等。
- • 执行阶段才是真正“解释”代码,做计算、调用函数、修改内存等。
- • 如果在执行阶段访问局部变量但它未初始化,就会报
UnboundLocalError。
看清编译字节码的结构
img粗看,dis反编译字节码中第2行出现两回重复了。其实是 模块级字节码 + 函数字节码 的区别。
a) 模块级字节码
0 0 RESUME 0 1 2 LOAD_CONST 0 (1) 4 STORE_NAME 0 (x) 2 6 LOAD_CONST 1 (<code object foo ...>) 8 MAKE_FUNCTION 0 10 STORE_NAME 1 (foo) 5 12 PUSH_NULL 14 LOAD_NAME 1 (foo) 16 CALL 0 24 POP_TOP 26 RETURN_CONST 2 (None)
解释:
- •
LOAD_CONST 1 (<code object foo ...>) - • Python 编译
foo 函数体时生成一个 code object,存在模块常量表里。
注意:这里 LOAD_CONST 1 的常量是函数的 code object,而不是源代码再次编译。
b) 函数字节码
Disassembly of <code object foo ...>: 2 0 RESUME 0 3 2 LOAD_GLOBAL 1 (NULL + print) 12 LOAD_FAST_CHECK 0 (x) 14 CALL 1 22 POP_TOP 4 24 LOAD_FAST 0 (x) 26 LOAD_CONST 1 (1) 28 BINARY_OP 13 (+=) 32 STORE_FAST 0 (x) 34 RETURN_CONST 0 (None)
解释:
- • 编译器在解析
def foo(): ... 时生成了一个独立的 code object。 - • 第 2 行在模块字节码里是
MAKE_FUNCTION,在函数字节码里是 RESUME 等指令。
好也,现在再说说作用域,为何函数里边是另一个作用域。
作用域规则(LEGB)
Python 的作用域遵循 LEGB 原则:
在函数里,如果你 给一个变量赋值,Python 会把这个变量 默认认为是局部变量(除非你用 global 或 nonlocal,比如global x)。
为什么 Python 看起来像解释执行?
- • CPython 在用户眼里是 一行一行执行,所以大家叫它“解释执行”。
- • 但是 每个函数或模块执行前都会先编译成字节码。
- • 这个编译是即时的(just-in-time 编译到字节码,不是机器码),不生成独立机器码文件,所以你感觉像解释执行。