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  
<code object at 0x0000028788A27660, file "", line 1>

可以看到,编译后返回了一个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的动态编译了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容