Python虽然是一门解释型语言,但Python程序执行时,也需要将源码进行编译生成字节码,然后由Python虚拟机进行执行,因此Python解释器实际是由两部分组成:编译器
和虚拟机
。
Python程序执行过程和Java类似,都是先将代码编译成字节码,然后由虚拟机执行:
编译Python程序
在Java中,使用javac
命令调用编译器(JavaCompile)
将.java文件编译成字节码并输出.class字节码文件,然后使用java
命令通过JVM
执行编译后的字节码文件;
在Python中,由于编译器和虚拟机合二为一,所以没有区分编译器和虚拟机运行的命令,使用python
命令调用python解释器
,默认就会将.py文件编译并运行。
py_compile.compile函数
但既然需要编译后运行,那当然会有编译功能模块,python可以调用compileall.py
或py_compile.py
模块来编译.py文件并生成.pyc字节码文件
compileall.py模块
compileall.py模块有compile_dir
、compile_file
两个个函数,用于对指定目录或文件进行编译。通过Python命令参数调用时,compile_path
函数会通过传入路径参数判断编译目标是目录还是文件
在桌面编写demo.py程序,使用python -m compileall demo.py
编译:
编译完成后,在demo.py同级目录下生成了一个
__pycache__
目录,目录下有一个demo.cpython-37.pyc
文件(python使用Python3.7版本):
更多关于compileall的内容详见:https://docs.python.org/zh-cn/3.7/library/compileall.html
py_compile.py模块
compileall内部最终调用的是py_compile.py模块的compile
函数,compile
函数是最终完成代码编译并输出字节码文件的函数,因此也可以使用python -m py_compile demo.py
对代码进行编译:
更多关于py_compile的内容详见:https://docs.python.org/zh-cn/3.7/library/py_compile.html
内建函数compile
python还有一个内建函数compile
,用于将源码编译成代码或 AST 对象。代码对象可以被 exec() 或 eval() 执行。源码可以是常规的字符串、字节字符串,或者 AST 对象。bltinmodule.c中compile函数的定义:
/*
source: object
filename: object(converter="PyUnicode_FSDecoder")
mode: str
flags: int = 0
dont_inherit: bool(accept={int}) = False
optimize: int = -1
*
_feature_version as feature_version: int = -1
将源代码编译成可由exec()或eval()执行的代码对象
source: 源代码可以是一个Python模块、语句或表达式
filename: 文件名将用于运行时错误消息
mode: 编译模块的模式必须是'exec',编译单个(交互式)语句的模式必须是'single',
编译表达式的模式必须是'eval'
...
*/
static PyObject *
builtin_compile_impl(PyObject _module, PyObject_ source, PyObject *filename,
const char *mode, int flags, int dont_inherit,
int optimize, int feature_version)
{
...C语言源码实现位于cpython/Python/bltinmodule.c,有兴趣的可以去github查看完整源码
}
compile函数使用方法:
- compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)
下面来看看compile的用法:
>>> source = 'for i in range(3): print(i)'
>>> result = compile(source, '', 'exec')
>>> result
可以看到,编译后返回了一个CodeObject对象,这个对象是可以被exec函数执行的:
>>> exec(result)
0
1
2
编译运行过程
从上述内容可知,py_compile.py模块中的compile函数由python实现,只能对.py文件进行编译,编译完成后返回字节码文件路径;
而内建函数compile由C语言实现,可以对代码语句进行编译,编译完成后返回一个CdoeObject对象。
那这个CodeObject对象是什么呢?
在上述的compile源码中,可以看到函数返回的是PyObject,这是返回给Python的包装对象,其内部是PyCodeObject,PyCodeObject对象如下:
/* _Bytecode object_ */
struct PyCodeObject {
PyObject_HEAD / _Python定长对象头_ /
PyObject *co_consts; /* 常量列表 */
PyObject *co_names; /* 名称列表(常量名、函数名、类名等) */
PyObject *co_code; /* 指令操作码(字节码) */
PyObject *co_filename; /* 源文件名 */
...完整属性请查看cpython/Include/cpython/code.h
};
查看result的属性值:
>>> result.co_names
('range', 'i', 'print')
>>> result.co_consts
(3, None)
>>> result.co_code
b'x\x18e\x00d\x00\x83\x01D\x00]\x0cZ\x01e\x02e\x01\x83\x01\x01\x00q\nW\x00d\x01S\x00'
>>> result.co_filename
''
其中co_code
属性是不可读的bytes数据,这时候就要用到Python内置的一个模块——dis
,dis 模块通过反汇编支持CPython的 bytecode 分析。
通过dis模块的disco
函数,可反汇编代码对象
-
disco
(code, lasti=-1, ***, file=None)
其中参数code是代码对象
disco方法返回字节码操作的格式化字符,描述了Python解释器将要执行的指令,内容类似于汇编语言,内容分为以下几列:
- 行号,用于每行的第一条指令
- 当前指令,表示为
-->
, - 一个指令 >> 表示,
- 指令的地址,
- 操作码名称,
- 操作参数,和括号中的参数解释。
例如:
>>> import dis
>>> dis.disco(result)
1 0 SETUP_LOOP 24 (to 26)
2 LOAD_NAME 0 (range)
4 LOAD_CONST 0 (3)
6 CALL_FUNCTION 1
8 GET_ITER
>> 10 FOR_ITER 12 (to 24)
12 STORE_NAME 1 (i)
14 LOAD_NAME 2 (print)
16 LOAD_NAME 1 (i)
18 CALL_FUNCTION 1
20 POP_TOP
22 JUMP_ABSOLUTE 10
>> 24 POP_BLOCK
>> 26 LOAD_CONST 1 (None)
28 RETURN_VALUE
SETUP_LOOP 24 (to 26)
:将一个用于循环的块推到块堆栈上。该块从当前指令开始,其大小为24字节
LOAD_NAME 0 (0)
:将result.co_names[0]的值移到栈顶,该值是'range'
LOAD_CONST 0 (0)
:将result.co_consts[0]移到栈顶,该值是3
CALL_FUNCTION 1
:调用一个可调用对象并传入位置参数1
POP_TOP
:删除栈顶元素,即删除TOS
GET_ITER
:实现TOS = iter(TOS),把 iter(TOS)
的结果推回堆栈
...
>>> result.co_names[0]
'range'
>>> result.co_consts[0]
3
综上所述,Python内建函数compile在编译某段源码时,不会直接返回字节码,而是返回一个CodeObject对象,该对象存储了程序运行所需的相关数据
和程序运行过程
。
更多操作码解释和dis模块的使用请查看:dis --- Python 字节码反汇编器
动态编译
Java程序运行时,会先编译所有Java文件并加载类,程序运行起来后无论修改或新增代码,都不会影响程序运行,如果需要在运行中加入新的代码,需要先调用编译器(JavaCompiler)编译代码,再调用类加载器(ClassLoader)将编译后的字节码加入当前的运行环境,这个过程就是动态编译。
在Python中,要实现程序运行后执行新增的代码要简单得多,Python提供一个内建函数exec
,可以执行代码语句或者是经过compile编译后的代码对象:
>>> help(exec)
Help on built-in function exec in module builtins:
exec(source, globals=None, locals=None, /)
Execute the given source in the context of globals and locals.
The source may be a string representing one or more Python statements
or a code object as returned by compile().
The globals must be a dictionary and locals can be any mapping,
defaulting to the current globals and locals.
If only globals is given, locals defaults to it.
所以,如果代码比较简单,可以直接调用exec执行Python语句字符串;如果代码比较复杂,可以先写入.py文件,调用compile编译代码后再调用exec执行。
综上所述,最多只需要调用两个方法,就可以实现Python的动态编译了。