编译过程不仅生成字节码,还要包含常量、变量、占用栈的空间等,Pyton中编译过程生成code对象PyCodeObject。将PyCodeObject写入二进制文件,即.pyc。
有必要则写入A.pyc指的是该.py是否只运行一次,如果import的模块,肯定会生成.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文件的函数包括以下几个步骤:
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值。
从pyc文件读入时,依靠list,那么序号就可以利用上了。