《Python源码剖析》读书笔记-7 Python的编译结果


第7章 Python的编译结果——Code对象与pyc文件

  • Python的执行流程大致是:Python源代码(.py)->Python解释器(python27.dll)->Python字节码(.pyc)->Python虚拟机(python27.dll)->执行结果
  • pyc文件中存储了编译的结果,一个嵌套的PyCodeObject对象,import会触发pyc文件的生成,import其实也就是把pyc文件中保存的PyCodeObject加载到内存中
  • PyCodeObject的定义如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    typedef struct {
    PyObject_HEAD
    int co_argcount; /* #arguments, except *args */
    int co_nlocals; /* #local variables */
    int co_stacksize; /* #entries needed for evaluation stack */
    int co_flags; /* CO_..., see below */
    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) */
    /* The rest doesn't count for hash/cmp */
    PyObject *co_filename; /* string (where it was loaded from) */
    PyObject *co_name; /* string (name, for reference) */
    int co_firstlineno; /* first source line number */
    PyObject *co_lnotab; /* string (encoding addr<->lineno mapping) See
    Objects/lnotab_notes.txt for details. */

    void *co_zombieframe; /* for optimization only (see frameobject.c) */
    PyObject *co_weakreflist; /* to support weakrefs to code objects */
    } PyCodeObject;
  • 从PyObject_HEAD宏可以看出它也是一个普通的PyObject对象。

  • pyc文件中的数据格式一般为 [数据类型标识符(Float:’f’,String: ‘s’…)]+[数据长度(可选)]+[具体数据]
  • pyc文件中是如何写入字符串的。
    字符串分三种类型:
    • 不需要Intern的普通字符串,标识符’s’,直接写入标识符+字符串长度+字符串
    • 需要Intern的字符串第一次出现,标识符’t’, 首先在WFILE对象的strings字典中保存(字符串->id)键值对,然后写入标识符+字符串长度+字符串
    • 需要Intern的字符串非第一次出现,标识符’R’, 首先从WFILE对象的strings字典中取出字符串对应的id值,然后写入标识符+id值
  • 当从pyc文件中恢复字符串时,也需要处理这三种情况(这时,WFILE对象中的strings指针则指向了一个PyListObject,而不是字典):
    • 标识符为’s’, 是为普通字符串,直接按照格式读取即可;
    • 标识符为’t’,是Intern的字符串第一次在pyc文件中出现,在按格式读出字符串之后,还需要将其添加到WFILE对象的strings数组的末尾;
    • 标识符为’R’,是Intern的字符串非第一次在pyc文件中出现,那么按格式读出其id,然后以该id为下表索引,取出WFILE对象的strings数组中的对应字符串。
  • 这样做的好处是避免了重复存储同一个字符串多次,大大减小了pyc文件的大小。但是python源代码中同一个字符串会出现多次吗?答案是会的,因为python源码中的每一个变量名,在编译中都会被看作是一个字符串,而变量名在python源码中是重复多次的。
  • co_lnotab存储了 (字节码偏移->python源代码行号)的映射,而其形式是unsigned byte数组,并且存储的并非原始值,而是增量:

    • [字节码偏移增量1,源代码行号增量1,字节码偏移增量2,源代码行号增量2,…]
    • 例如,如果co_lnotab中存储的是这样一个数组:

      1
      [0, 1, 6, 1, 44, 5]
    • 那么实际上的映射为:

      0             --->       1
      0+6=6         --->       1+1=2
      0+6+44=50     --->       1+1+5=7
      
    • 为啥要存增量呢?我猜测,这里为了节省内存,co_lnotab中的每一个元素都只用了一个byte,其最大值也就是255。存储原始值,岂不是要限制字节码偏移值和源代码行数都得在256行之内?!所以还是存储增量靠谱,编译器可以用来保证编译出的相邻字节码之间的偏移以及对应的源代码偏移都不要超过255。

欢迎关注我的微信公众号,技术·生活·思考:
后端技术小黑屋

Comments