Python内置compile函数与多模块编译机制解析
多模块Python代码的编译与导入机制解析
问题背景
我正在学习Python的compile()函数与dis模块,单模块代码里能查看每行的编译过程,但想搞懂多模块的编译机制,于是准备了两个文件:
test.py
import sys from temp import doubler def tripler(n: int) -> int: f = 3 * n return f if __name__ == '__main__': n = int(sys.argv[1]) if len(sys.argv) > 1 else 4 t = tripler(n) s = doubler(t) print(s)
temp.py
def doubler(x): y = 2 * x return y
对test.py文本执行compile()后用dis.dis()得到的输出:
0 0 RESUME 0 1 2 LOAD_CONST 0 (0) 4 LOAD_CONST 1 (None) 6 IMPORT_NAME 0 (sys) 8 STORE_NAME 0 (sys) 2 10 LOAD_CONST 0 (0) 12 LOAD_CONST 2 (('doubler',)) 14 IMPORT_NAME 1 (temp) 16 IMPORT_FROM 2 (doubler) 18 STORE_NAME 2 (doubler) 20 POP_TOP 4 22 LOAD_CONST 3 ('n') 24 LOAD_NAME 3 (int) 26 LOAD_CONST 4 ('return') 28 LOAD_NAME 3 (int) 30 BUILD_TUPLE 4 32 LOAD_CONST 5 (<code object tripler at 0x00000255AB98F910, file "test.py", line 4>) 34 MAKE_FUNCTION 4 (annotations) 36 STORE_NAME 4 (tripler) 9 38 LOAD_NAME 5 (__name__) 40 LOAD_CONST 6 ('__main__') 42 COMPARE_OP 40 (==) 46 POP_JUMP_IF_FALSE 69 (to 186) 10 48 PUSH_NULL 50 LOAD_NAME 6 (len) 52 LOAD_NAME 0 (sys) 54 LOAD_ATTR 14 (argv) 74 CALL 1 82 LOAD_CONST 7 (1) 84 COMPARE_OP 68 (>) 88 POP_JUMP_IF_FALSE 21 (to 132) 90 PUSH_NULL 92 LOAD_NAME 3 (int) 94 LOAD_NAME 0 (sys) 96 LOAD_ATTR 14 (argv) 116 LOAD_CONST 7 (1) 118 BINARY_SUBSCR 122 CALL 1 130 JUMP_FORWARD 1 (to 134) >> 132 LOAD_CONST 8 (10) >> 134 STORE_NAME 8 (n) 11 136 PUSH_NULL 138 LOAD_NAME 4 (tripler) 140 LOAD_NAME 8 (n) 142 CALL 1 150 STORE_NAME 9 (t) 12 152 PUSH_NULL 154 LOAD_NAME 2 (doubler) 156 LOAD_NAME 9 (t) 158 CALL 1 166 STORE_NAME 10 (s) 13 168 PUSH_NULL 170 LOAD_NAME 11 (print) 172 LOAD_NAME 10 (s) 174 CALL 1 182 POP_TOP 184 RETURN_CONST 1 (None) 9 >> 186 RETURN_CONST 1 (None) Disassembly of <code object tripler at 0x00000255AB98F910, file "test.py", line 4>: 4 0 RESUME 0 5 2 LOAD_CONST 1 (3) 4 LOAD_FAST 0 (n) 6 BINARY_OP 5 (*) 10 STORE_FAST 1 (f) 7 12 LOAD_FAST 1 (f) 14 RETURN_VALUE
我能看到从temp模块导入了doubler符号,但看不到它对应的code object,也不清楚它是在哪个阶段被编译的,而代码对象能正常执行。请问有人能解释temp模块的处理过程吗?
解答
1. compile()仅处理当前模块,不触发依赖编译
你调用compile()处理test.py文本时,这个操作只会编译当前模块的代码,不会主动去读取、编译依赖的temp.py。test.py里的from temp import doubler在编译阶段只会被转换成IMPORT_NAME、IMPORT_FROM这类字节码指令,此时Python还不会处理temp模块的内容。
2. temp模块的编译与加载发生在运行时
当test.py的字节码运行到IMPORT_NAME temp指令时,Python才会启动temp模块的加载流程:
- 先检查
sys.modules,确认temp模块是否已被加载; - 若未加载,找到
temp.py文件并读取内容,调用compile()将其编译为字节码(生成对应的code object); - 执行temp模块的字节码,定义
doubler函数(此时doubler的code object会被创建并关联到函数对象上); - 通过
IMPORT_FROM指令把doubler函数对象导入到test模块的命名空间中。
3. 看不到doubler的code object的原因
你是直接对test.py编译后的字节码反汇编,而temp模块的code object是在运行时才生成的,不会包含在test模块的编译结果里。如果想要查看doubler的字节码,可以在运行时添加代码:
import dis dis.dis(doubler)
或者直接对temp.py单独执行compile()和dis.dis()。
4. 模块编译的缓存机制
Python会把编译后的字节码缓存到__pycache__目录下的.pyc文件中,下次导入temp模块时,若temp.py未修改,会直接加载缓存的字节码,跳过重新编译步骤。
内容的提问来源于stack exchange,提问作者ACBlue
相关产品推荐
相关产品推荐

