Python编译和运行

Python虽然是一门解释型语言,但Python程序执行时,也需要将源码进行编译生成字节码,然后由Python虚拟机进行执行,因此Python解释器实际是由两部分组成:编译器虚拟机
Python程序执行过程和Java类似,都是先将代码编译成字节码,然后由虚拟机执行:

Python执行过程

编译Python程序

在Java中,使用javac命令调用编译器(JavaCompile)将.java文件编译成字节码并输出.class字节码文件,然后使用java命令通过JVM执行编译后的字节码文件;

在Python中,由于编译器和虚拟机合二为一,所以没有区分编译器和虚拟机运行的命令,使用python命令调用python解释器,默认就会将.py文件编译并运行。

py_compile.compile函数

但既然需要编译后运行,那当然会有编译功能模块,python可以调用compileall.pypy_compile.py模块来编译.py文件并生成.pyc字节码文件

compileall.py模块

compileall.py模块有compile_dircompile_file两个个函数,用于对指定目录或文件进行编译。通过Python命令参数调用时,compile_path函数会通过传入路径参数判断编译目标是目录还是文件
在桌面编写demo.py程序,使用python -m compileall demo.py编译:

compileall编译py文件

编译完成后,在demo.py同级目录下生成了一个__pycache__目录,目录下有一个demo.cpython-37.pyc文件(python使用Python3.7版本):
生成的pyc文件

更多关于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编译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解释器将要执行的指令,内容类似于汇编语言,内容分为以下几列:

  1. 行号,用于每行的第一条指令
  2. 当前指令,表示为 -->
  3. 一个指令 >> 表示,
  4. 指令的地址,
  5. 操作码名称,
  6. 操作参数,和括号中的参数解释。

例如:

>>> 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的动态编译了。

你可能感兴趣的:(Python编译和运行)