Python内核阅读(七): pyc文件与code对象

Python 2017-08-06

起步

python对源程序编译结果是生成一个 .pyc 文件. python对 .py 文件的编译结果是字节码, 为了能复用而不需要重新编译才有了写成.pyc文件. 对于解释器来说PyCodeObject对象才是真正编译结果, pyc文件只是这个对象在硬盘上的表现形式.

PyCodeObject

[code.h]
typedef struct {
    PyObject_HEAD
    int co_argcount;        /* #arguments, except *args */
    int co_kwonlyargcount;  /* #keyword only arguments */
    int co_nlocals;     /* #local variables */
    int co_stacksize;       /* #entries needed for evaluation stack */
    int co_flags;       /* CO_..., see below */
    int co_firstlineno;   /* first source line number */
    PyObject *co_code;      /* instruction opcodes */
    PyObject *co_consts;    /* list (constants used) */
    PyObject *co_names;     /* list of strings (names used) */
    PyObject *co_varnames;  /* tuple of strings (local variable names) */
    PyObject *co_freevars;  /* tuple of strings (free variable names) */
    PyObject *co_cellvars;      /* tuple of strings (cell variable names) */
    void *co_extra;
} PyCodeObject;

编译器在对源代码进行编译的时候, 每一个 Code Block 会创建一个 PyCodeObject 对象与这个代码段相对应. 代码段的范围可大可小. 可以是整个py文件, 可以是class, 可以是函数.

一个Code Block也对应了一个名字空间. Field Content
co_argcount Code Block 的位置参数的个数
co_nlocals 局部变量的个数
co_stacksize 执行该段Code Block 需要的栈空间
co_firstlineno 对应.py文件的起始行
co_code Code Block 编译所得的字节码, 以PyBytesObject形式存在
co_consts PyTupleObject ,保存Code Block中的所有常量
co_names PyTupleObject, 保存Code Block中所有符号
co_varnames 局部变量名集合
co_freevars 实现闭包时用到的
co_cellvars 嵌套函数所引用的局部变量集合
co_filename .py文件的完整路径
co_lnotab 字节码指令与.py中行号对应关系, 以PyBytesObject形式存在

访问PyCodeObject对象

在python中, 有与c一级的对PyCodeObject简单包装, code对象, 可以访问到PyCodeObject的各个域.

>>> source = open('db.py').read()
>>> co = compile(source, 'db.py', 'exec')
>>> type(co)
<class 'code'>
>>> co.co_names
('pymysql', 'config', 'threading', 'RLock', 'Lock', 'create_table_template', 'ob
ject', 'Model', 'str', 'm')

写入文件 PyMarshal_WriteObjectToFile

向pyc文件写入数据主要是这几个, 有删减:

[marshal.c]
typedef struct {
    FILE *fp;
    int depth;
    PyObject *str;
    char *ptr;
    char *end;
    char *buf;
    _Py_hashtable_t *hashtable;
    int version;
} WFILE;

#define w_byte(c, p) do {                               \
        if ((p)->ptr != (p)->end || w_reserve((p), 1))  \
            *(p)->ptr++ = (c);                          \
    } while(0)

static void w_flush(WFILE *p)
{
    assert(p->fp != NULL);
    fwrite(p->buf, 1, p->ptr - p->buf, p->fp);
    p->ptr = p->buf;
}

这是文件写入定义的基本结构, fp指向最后要写入的文件, w_byte(c, p) 则是一个简单的封装, 以字节为单位的复制到p->ptr先行区中. w_flush(WFILE *p) 则是将缓冲区 p->buf 写到文件中.p->ptr也就是准备写到文件中的增量部分.

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(const char *s, Py_ssize_t n, WFILE *p)
{
    Py_ssize_t m;
    if (!n || p->ptr == NULL)
        return;
    m = p->end - p->ptr;
    if (p->fp != NULL) {
        if (n <= m) {
            memcpy(p->ptr, s, n);
            p->ptr += n;
        }
        else {
            w_flush(p);
            fwrite(s, 1, n, p->fp);
        }
    }
    else {
        if (n <= m || w_reserve(p, n - m)) {
            memcpy(p->ptr, s, n);
            p->ptr += n;
        }
    }
}

如在调用 PyMarshal_WriteLongToFile 时, 会调用w_long, 数据将会一个字节字节的写入到文件中. 而调用PyMarshal_WriteObjectToFile也会调用w_object , 这个函数比较长,就不列出来了.

为了区分写入的类型, 在写入文件前会做一个动作,就是先将待写入的对象类型写进去:

[marshal.c]
#define TYPE_NULL               '0'
#define TYPE_NONE               'N'
#define TYPE_FALSE              'F'
#define TYPE_TRUE               'T'
#define TYPE_STOPITER           'S'

#define W_TYPE(t, p) do { \
    w_byte((t) | flag, (p)); \
} while(0)

这个对象类型的标识对读取pyc文件至关重要, 因为对象写入pyc文件后, 所有数据都变成字节流, 类型信息丢失. 有了这个标识, 当读取这样的标识时, 则预示着上一个对象结束, 新的对象开始, 也能知道新对象是什么类型的.

内部也有机制处理共享对象, 减少字节码中冗余信息. 共享类型的属于 TYPE_INTERNED.

加载pyc文件 PyMarshal_ReadObjectFromFile

看一下加载pyc文件的过程, 让pyc文件理解更加深刻:

PyObject * PyMarshal_ReadObjectFromFile(FILE *fp)
{
    RFILE rf;
    PyObject *result;
    rf.fp = fp;
    rf.readable = NULL;
    rf.current_filename = NULL;
    rf.depth = 0;
    rf.ptr = rf.end = NULL;
    rf.buf = NULL;
    rf.refs = PyList_New(0);
    if (rf.refs == NULL)
        return NULL;
    result = r_object(&rf);
    Py_DECREF(rf.refs);
    if (rf.buf != NULL)
        PyMem_FREE(rf.buf);
    return result;
}

r_object 开始就开始从pyc文件中读入数据, 并创建PyCodeObject对象, 这个 r_object 是对 w_object 的逆运算. 当读到 TYPE_INTERNED 后, 会将其后面的字符串读入, 将这个字符串进行intern操作.

多个PyCodeObject

Code Block可以是整个文件, 也可以是某个函数. 因此PyCodeObject也存在嵌套关系. 这就是 co_consts 的作用. 在w_object中保存code对象时会是一个递归的调用.


本文由 hongweipeng 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

如果对您有用,您的支持将鼓励我继续创作!