Python 编译:code对象 与 pyc文件

运行程序

当在shell中敲入python xx.py运行 Python 程序时,就是激活了 Python 解释器。

Python 解释器并不会立即运行程序,而是会对 Python 程序的源代码进行编译,产生字节码,然后将字节码交给虚拟机一条条顺序执行。

源文件中的内容可以分为:字符串常量操作

操作会被编译为字节码指令序列字符串常量在编译的过程中会被收集起来。这些编译后的信息在程序运行时,会作为 运行时对象 PyCodeObject 存储于内存中。运行结束后,PyCodeObject 被放入xx.pyc文件,保存在硬盘中。这样,在下次运行时,可以直接根据.pyc文件的内容,在内存中建立 PyCodeObject ,不需要再进行编译。

PyCodeObject

在编译器对源码进行编译时,会为每一个 Code Block 创建一个对应的 PyCodeObject。那么,什么是 Code Block 呢?规则是:当进入一个新的名字空间,或者新的作用域,就是进入了一个新 Code Block。名字空间是符号的上下文环境,决定了符号的含义。也就是说,决定了变量名对应的变量值是什么。

名字空间是可以嵌套的,能够形成一个名字空间链,虚拟机在执行字节码时,一个重要的任务就是从链中确定一个符号的对象是什么。

在 Python 中,类、函数、modules 对应独立的名字空间,所以都有对应的 PyCodeObject。

PyCodeObject 中co_code域保存的就是对操作编译生成的字节码指令序列

产生pyc文件的方法

上面提到,Python 程序运行结束后,会在硬盘中以.pyc文件的形式存储 PyCodeObject,但直接运行 Python 程序并不会产生.pyc文件。

这可能是因为直接运行的 Python 程序,有些只是临时使用一次,所以没有通过.pyc保存编译结果的必要。

一种常见的,产生pyc文件的方法是import机制。当Python 程序运行时,如果遇到 import abc,会到设定好的path中寻找 abc.pyc 文件,如果没有,只找到abc.py,会先将 abc.py 编译成 CodeObject,然后创建 pyc 文件,将 CodeObject写入,最后才会对 pyc 进行import操作,将 pyc 中的 CodeObject重新复制到内存,并运行。

另外,Python 标准库中的py_compilecompile可以帮助手动产生 pyc 文件。

pyc 文件内容是二进制的,想要了解 pyc 文件的格式,就要了解 PyCodeObject 中各个域的作用。

PyCodeObject域

在 Python 中访问 PyCodeObject

C语言形式的 PyCodeObject 对应 Python 中的 Code对象,Code对象 是对 PyCodeObject 的简单包装。

因此,可以通过 Code对象 访问 PyCodeObject 的各个域。这就需要使用 内建函数 compile。

test.py

创建 pyc 文件

一个 pyc 文件包含三部分独立的信息:

  • magic number
  • pyc 文件的创建时间信息
  • PyCodeObject

import.c

下面一一进行说明

1,magic number
是 Python 定义的一个整数值,不同版本定义不同,用来确保 Python 的兼容性。Python 在加载 pyc 时首先检查 magic number ,如果与 Python 自身的 magic number 不同,说明创建 pyc 的 Python 版本 与 当前版本不兼容,会拒绝加载。

为什么会不兼容呢?因为字节码指令发生了变化,有删除或增加。

2,pyc 创建时间
使得 Python 自动将 pyc 文件与最新的 Python 文件同步。当对 Python 程序进行编译产生 pyc 后,如果后来进行了修改,此时 Python 在尝试加载 pyc 时,会发现 pyc 创建时间早于 Python 程序,于是将重新编译,生成新的 pyc 文件。

3,PyCodeObject
编译器会遍历 PyCodeObject 中的所有域,并依次写入 pyc。对于 PyCodeObject 中的每一个对象,同样会进行遍历,并写入类型标志数据(数值/字符串)

类型标志的三个作用:表明上一个对象的结束、新对象的开始、确定新对象的类型

marshal.h,类型标志

向 pyc 写入字符串

部分略

对于嵌套的名字空间,产生的 PyCodeObject 也是递归嵌套的,嵌套的 PyCodeObject 在上层 PyCodeObject 的co_consts中。

字节码

源代码编译为 字节码指令 序列,虚拟机根据字节码进行操作,完成程序的执行,opcode.h中定义了当前版本 Python 支持的字节码指令。

字节码指令 的编码并不是按顺序增长的,中间有跳跃。

Include目录下的opcode.h定义了字节码指令

解析 pyc

由于包含嵌套 PyCodeObject,pyc 中的二进制数据实际上是有结构的,可以以 XML格式进行解析,从而可视化。使用 pycparser。

而 Python 库中 dis 的 dis 方法可以对 code对象 进行解析。接收 code对象,输出 字节码指令信息。

dis.dis 的输出:

  • 第一列,是 字节码指令 对应的 源代码 在 Python 程序中的行数
  • 第二列,是当前 字节码指令 在 co_code 中的偏移位置
  • 第三列,当前的字节码指令
  • 第四列,当前字节码指令的参数

test.py

参考资料

《Python 源码剖析》第七章

1 收藏 评论

可能感兴趣的话题



直接登录
跳到底部
返回顶部