python中编译过程解析:pyc文件的作用及工作原理

python作为解释性语言,其解释机制和过程是什么呢?让我们慢慢看来!

首先,可以确认的是python解释执行的基础依据是我们大家都熟悉的py文件,那么是不是解释器就是读取并解析py文件来执行呢?有点计算机背景的人肯定会鄙视这种看法的。实际上python也要进行一种类似于编译的动作,其读取py文件生成pyc文件。那么pyc文件中又是什么呢?

在这里我们不谈词法/语法分析等编译细节,python编译器读取py文件,生成一个对象,what? 对!生成一个PyCodeObject,这个东东详细描述了其对应python代码块,也就是说python的编译器经过词法/语法/指令分析后,将py文件‘重绘’成一个对象。所谓的pyc 文件就是这个对象的内存映像(以磁盘文件的形式展现)。

typedef struct {
    PyObject_HEAD
    int co_argcount;
    int co_nlocals;
    int co_stacksize;
    int co_flags;
    PyObject *co_code;
    PyObject *co_consts;
    PyObject *co_names;
    PyObject *co_varnames;
    PyObject *co_freevars;
    PyObject *co_cellvars;
    /* The rest doesn't count for hash/cmp */
    PyObject *co_filename;
    PyObject *co_name;
    int co_firstlineno;
    PyObject *co_lnotab;
    void *co_zombieframe;
} PyCodeObject;

其中需要关注的是:

  1. co_code,其存放的就是py文件的字节码序列,以python的StringObject形式存放;
  2. co_lnotab,存放的是字节码序列与py文件的行号对应,想起python报错信息了吗?
  3. co_consts,存放的是python程序中的所有常量,会出现嵌套,即某个常量可能又是一个PyCodeObject对象
  4. co_names,存放python程序中的变量名?
  5. co_varnames,存放python程序中的局部变量名?

跳过编译过程,直接看当编译程序将py文件分析并且生成PyCodeObject后,如何写入(生成)pyc文件的源码:write_compiled_module,我们看到其中写入:

  1. python版本号:python版本兼容检查
  2. 编译完成时间:最新的pyc文件代表最新的py文件
  3. 通过调用PyMarshal_WriteObjectToFile,将PyCodeObject写入到pyc文件:提高python文件的启动速度

现在我们对PyCodeObject有了一个大概的了解了,那么我们就在想它是如何写入文件(pyc文件)的,其实写入文件也很简单:open("***.pyc",'w').write(str(pycodeobject))!这样是爽了,可是待会你如何将它们再还原回去?

想想,要费很大的力气来确定一个变量的开始与结束。。。,这样直接将dict以key:value的形式写入,对人来说比较易读,但对于机器来说难度较高。那么如何来让机器快速从文件中还原出PyCodeObject呢?

  • ”前面的多态思想“,对,让机器知道value的类型,根据类型分发还原方法;
  • 对于python的定长数据类型,机器可以明确知道读取多长的内容是指定变量的内容;但对于变长数据类型,机器就很难来判断。此时若告诉机器该数据的元素个数,就很容易还原了;

这样看来,我们需要将PyCodeObject以结构化的数据保存在文件中(pyc文件),下面就详细分析下,python如何结构化的写入,又如何结构化的还原回来的:

python编译完py文件后,得到一个PyCodeObject对象后,会在import.c中调用write_compiled_module,在这里我们看到其除了写通过调用PyMarshal_WriteObjectToFile来写入PyCodeObject外,还需要写入前面提到的版本号与编译时间,我们就来看看PyMarshal_WriteObjectToFile中到底发生了什么?

void
PyMarshal_WriteObjectToFile(PyObject *x, FILE *fp, int version)
{
WFILE wf;
wf.fp = fp;
wf.error = 0;
wf.depth = 0;
wf.strings = (version > 0) ? PyDict_New() : NULL;
wf.version = version;
w_object(x, &wf);
Py_XDECREF(wf.strings);
}

调用了w_object,一个PyCodeObject,就只调用了一次就写完了? 想到了吗? ===递归,对,w_object内部就是递归调用来完成整个PyCodeObject的写入的。我们现仔细想一想,对于任何一门程序而言,不管你上层逻辑如何复杂/数据类型如何多,最终只归结为两类数据:数字与字符。因此对于python也是如此,因此可以想像一下,w_object中的所有写入操作最终都会归结为数字写入与字符写入。话虽然是这样说,但是因为不同的数据类型,数字与字符组织方式是不同的:比如list与dict就不一样,因此还是要对不同的数据类型进行一些简单的分发。这其中对string的处理最为复杂,因为其牵扯到一个interned机制。这里我们重点学习一下python的写入/读出操作时,对于interned机制的利用:

  • 其在写入时,使用了一个dict来索引所有string,其中key为string,value为出现序号;
  • 其在读出时,使用一个list来索引所有的string,其中value就是string

这是为什么呢?其实仔细一想,挺简单的,写入时,当我们碰到一个字符串,我知道的信息只有这个串,因此我们需要构造一个dict,用串作为key。读出时,其解析顺序与写入时一致,因此,我们发现一个非首次interned串时,其首次interned串早已经写入list,如此一来,我们直接用当时写入的序号来索引list就OK了。简单吧,奇妙吧!

下面该写pyc解析程序了:

 

发表评论