Python编译结果之code对象与pyc文件详解
1、python程序执行过程
与java类似,python将.py编译为字节码,然后通过虚拟机执行。编译过程与虚拟机执行过程均在python25.dll中。python虚拟机比java更抽象,离底层更远。
编译过程不仅生成字节码,还要包含常量、变量、占用栈的空间等,pyton中编译过程生成code对象pycodeobject。将pycodeobject写入二进制文件,即.pyc。
有必要则写入a.pyc指的是该.py是否只运行一次,如果import的模块,肯定会生成.pyc。
2、pycodeobject对象与.pyc文件
python解释器将.py程序编译为pycodeobject对象,具体过程与编译原理类似。
typedef struct { pyobject_head int co_argcount; // code block的参数的个数,比如说一个函数的参数 int co_nlocals; // code block中局部变量的个数 int co_stacksize; // 执行该段code block需要的栈空间 int co_flags; // n/a pyobject *co_code; // code block编译所得的byte code,以pystringobject的形式存在 pyobject *co_consts; // pytupleobject对象,保存code block中的常量 pyobject *co_names; // pytupleobject对象,保存code block中的所有符号 pyobject *co_varnames; // code block中局部变量名集合 pyobject *co_freevars; // 实现闭包所需东西 pyobject *co_cellvars; // code block内部嵌套函数所引用的局部变量名集合 pyobject *co_filename; // code block所对应的.py文件的完整路径 pyobject *co_name; // code block的名字,通常是函数名或类名 int co_firstlineno; // code block在对应的.py文件中的起始行 pyobject *co_lnotab; // byte code与.py文件中source code行号的对应关系,以pystringobject的形式存在 void *co_zombieframe; pyobject *co_weakreflist; } pycodeobject;
一个code block生成一个pycodeobject,进入一个名字空间成为进入一个code block。如下.py文件编译完成后会生成三个pycodeobject,一个对应整个.py文件一个对应class a,一个对应def fun。实际这三个code对象是嵌套的,后两个code对象位于第一个code对象的co_consts属性中。其实,字节码位于co_code中。
class a: pass def fun(): pass a = a() fun()
pyc文件包括三部分:
(1)四字节的magic int,表示pyc版本信息
(2)四字节的int,是pyc产生时间,若与py文件时间不同会重新生成
(3)序列化了的pycodeobject对象。
3、pyc文件的生成
写入pyc文件的函数包括以下几个步骤:
pymarshal_writelongtofile(pyc_magic, fp, py_marshal_version); // 写入版本信息 pymarshal_writelongtofile(0l, fp, py_marshal_version); // 写入时间信息 pymarshal_writeobjecttofile((pyobject *)co, fp, py_marshal_version); // 写入pycodeobject对象
关键在于code对象的写入:
{ wfile wf; wf.fp = fp; …… w_object(x, &wf); }
用到了一个wfile结构体,可以认为是对file *fp 的一个封装:
typedef struct { file *fp; int error; int depth; pyobject *strings; // 存储字符串,写入时以dict形式,读出时以list形式 } wfile;
关键在于w_object()函数:
static void w_object(pyobject *v, wfile *p){ if (v == null) …… else if (pyint_checkexact(v)) …… else if (pyfloat_checkexact(v)) …… else if (pystring_checkexact(v)) …… else if (pylist_checkexact(v)) …… }
w_code实质为根据不同的对象类型选取不同的策略,例如tuple对象:
else if (pytuple_checkexact(v)) { w_byte(type_tuple, p); n = pytuple_size(v); w_size(n, p); for (i = 0; i < n; i++) w_object(pytuple_get_item(v, i), p);
而所有类型最终可分解为写入数值与写入字符串两种操作,涉及以下几部分:
#define w_byte(c, p) putc((c), (p)->fp) // 用于写入类型 static void w_long(long x, wfile *p){ // 用于写入数字 w_byte((char)( x & 0xff), p); // 实质为用四个字节存储一个数字 w_byte((char)((x>> 8) & 0xff), p); w_byte((char)((x>>16) & 0xff), p); w_byte((char)((x>>24) & 0xff), p); } static void w_string(char *s, int n, wfile *p){ //用于写入字符串 fwrite(s, 1, n, p->fp); }
由于序列化写入文件后丢失了结构信息,故写入每个对象时写入类型信息w_byte:
#define type_int 'i' #define type_list '[' #define type_dict '{' #define type_code 'c'
由于python皆对象,w_object(pyobject*)便可针对不同类型选取不同写入方法,不断细分,最终分解为pyint_object或pystring_object,利用w_long或w_string写入。
数字比较简单:
else if (pyint_checkexact(v)) { w_byte(type_int, p); w_long(x, p); }
字符串则比较复杂:
else if (pystring_checkexact(v)) { if (p->strings && pystring_check_interned(v)) { pyobject *o = pydict_getitem(p->strings, v); // 获取在strings中的序号 if (o) { // inter对象的非首次写入 long w = pyint_aslong(o); w_byte(type_stringref, p); w_long(w, p); goto exit; } else { // intern对象的首次写入 int ok; ok = o && pydict_setitem(p->strings, v, o) >= 0; py_xdecref(o); w_byte(type_interned, p); } } else { // 写入普通string w_byte(type_string, p); } n = pystring_get_size(v); w_size(n, p); w_string(pystring_as_string(v), n, p); }
(1)若写入普通字符串,写入字符串类型信息"s",然后写入字符串长度及string值。
(2)若写入inter字符串,先到wfile的strings中查找:
(a)若找到,则写入引用类型信息"r",然后写入序号
(b)若未找到,创建对象放入strings,并写入intern类型信息"t",然后写入字符串长度及string值。
若依次写入"efei"、"snow"、"efei",则会如下:
从pyc文件读入时,依靠list,那么序号就可以利用上了。
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注的更多内容!
上一篇: Python中装饰器的基本功能理解