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

推荐阅读更多精彩内容

  • 1. 简单的例子 先从一个简单的例子说起,包含了两个文件 foo.py 和 demo.py 执行这个程序pytho...
    jiangmo阅读 1,277评论 0 5
  • 模块和包 一 模块 1 什么是模块? 常见的场景:一个模块就是一个包含了python定义和声明的文件,文件名就是...
    go以恒阅读 2,081评论 0 4
  • 两本不错的书: 《Python参考手册》:对Python各个标准模块,特性介绍的比较详细。 《Python核心编程...
    静熙老师哈哈哈阅读 3,070评论 0 80
  • 解释型语言,由Python解释器把源文件编译为pyc文件(字节码文件), 然后让Python虚拟机去运行字节码 链...
    yangqi916阅读 1,261评论 0 0
  • 1. 枚举 - enumerate 可以有参数哦 之前我们这样操作: 现在我们这样操作: enumerate函数还...
    妄心xyx阅读 4,303评论 1 252
  • python一直被病垢运行速度太慢,但是实际上python的执行效率并不慢,慢的是python用的解释器Cpyth...
    ___n阅读 3,539评论 4 75
  • 模式和被搜索的字符串既可以是 Unicode 字符串 (str) ,也可以是8位字节串 (bytes)。 ...
    山海皆可平z阅读 176评论 0 0
  • 个人笔记,方便自己查阅使用 Py.LangSpec.Contents Refs Built-in Closure ...
    freenik阅读 67,384评论 0 5
  • 摘要:《零基础学Python》一书的学习记录,学习笔记。 写在前面的话:暑假过去一半了,辗转在Java视频学习与书...
    iamblackcat阅读 128评论 0 2
  • 新手教学,代码创造无限可能。祝君好运!!!!!!有用的话希望给点个赞。。。 1. 枚举 - enumerate 可...
    糖僧_8785阅读 382评论 0 0
  • Python 是一门解释型的编程语言,因此它具有解释型语言的运行机制。 计算机程序,其实就是一组计算机指令,能真正...
    卓尔不群的雅典阅读 154评论 0 0
  • 发现这一篇对 Python 源码树更详细的分析文章,特复制转载到此,感谢原文作者分享 作者:秘塔科技算法研究员Qi...
    爱吃鱼de大猫阅读 941评论 0 3
  •   模式和被搜索的字符串既可以是 Unicode 字符串 (str) ,也可以是 8 位字节串 (bytes)。 ...
    殉道者之花火阅读 381评论 0 1
  • 1.1 Python简介 [if !supportLists]• [endif]Python是一种面向对象、解释型...
    Chliao阅读 334评论 0 1
  • 第一部分 初识Python语言 第1章 程序设计基本方法 1.1 计算机的概念 计算机是根据指令操作数据的设备,具...
    不脱发的程序员阅读 761评论 0 0
  • ![Flask](data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAW...
    极客学院Wiki阅读 6,251评论 0 3
  • 不知不觉易趣客已经在路上走了快一年了,感觉也该让更多朋友认识知道易趣客,所以就谢了这篇简介,已做创业记事。 易趣客...
    Physher阅读 2,939评论 1 2
  • 双胎妊娠有家族遗传倾向,随母系遗传。有研究表明,如果孕妇本人是双胎之一,她生双胎的机率为1/58;若孕妇的父亲或母...
    邺水芙蓉hibiscus阅读 3,197评论 0 2
  • 今天理好了行李,看到快要九点了,就很匆忙的洗头洗澡,(心存一份念想,你总会打给我的🐶)然后把洗头液当成沐浴液了😨,...
    bevil阅读 2,436评论 1 1
  • 那年我们15,像阳光一样温暖的年纪。每天我都会骑自行车上学,路过田野,工厂,医院,村庄,有微风,有阳光,有绿...
    木偶说爱你阅读 2,120评论 0 3