Python 编译:code对象 与 pyc文件,pythonpyc, Python 解释器并


运行程序

当在shell中敲入python xx.py运行 Python 程序时,就是激活了 Python 解释器。

Python 解释器并不会立即运行程序,而是会对 Python 程序的源代码进行编译,产生字节码,然后将字节码交给虚拟机一条条顺序执行。

源文件中的内容可以分为:字符串常量操作

操作会被编译为字节码指令序列字符串常量在编译的过程中会被收集起来。这些编译后的信息在程序运行时,会作为 运行时对象 PyCodeObject 存储于内存中。运行结束后,PyCodeObject 被放入xx.pyc文件,保存在硬盘中。这样,在下次运行时,可以直接根据.pyc文件的内容,在内存中建立 PyCodeObject ,不需要再进行编译。

PyCodeObject

在编译器对源码进行编译时,会为每一个 Code Block 创建一个对应的 PyCodeObject。那么,什么是 Code Block 呢?规则是:当进入一个新的名字空间,或者新的作用域,就是进入了一个新 Code Block。名字空间是符号的上下文环境,决定了符号的含义。也就是说,决定了变量名对应的变量值是什么。

名字空间是可以嵌套的,能够形成一个名字空间链,虚拟机在执行字节码时,一个重要的任务就是从链中确定一个符号的对象是什么。

在 Python 中,类、函数、modules 对应独立的名字空间,所以都有对应的 PyCodeObject。

PyCodeObject 中co_code域保存的就是对操作编译生成的字节码指令序列

产生pyc文件的方法

上面提到,Python 程序运行结束后,会在硬盘中以.pyc文件的形式存储 PyCodeObject,但直接运行 Python 程序并不会产生.pyc文件。

这可能是因为直接运行的 Python 程序,有些只是临时使用一次,所以没有通过.pyc保存编译结果的必要。

一种常见的,产生pyc文件的方法是import机制。当Python 程序运行时,如果遇到 import abc,会到设定好的path中寻找 abc.pyc 文件,如果没有,只找到abc.py,会先将 abc.py 编译成 CodeObject,然后创建 pyc 文件,将 CodeObject写入,最后才会对 pyc 进行import操作,将 pyc 中的 CodeObject重新复制到内存,并运行。

另外,Python 标准库中的py_compilecompile可以帮助手动产生 pyc 文件。

pyc 文件内容是二进制的,想要了解 pyc 文件的格式,就要了解 PyCodeObject 中各个域的作用。

PyCodeObject域

在 Python 中访问 PyCodeObject

C语言形式的 PyCodeObject 对应 Python 中的 Code对象,Code对象 是对 PyCodeObject 的简单包装。

因此,可以通过 Code对象 访问 PyCodeObject 的各个域。这就需要使用 内建函数 compile。

test.py

import sys

a = 1

def b():
    print a
    a = 2
    print a

>>> source = open('/Users/chao/Desktop/test.py').read()
>>> co = compile(source, 'test.py', 'exec')
>>> type(co)
<type 'code'>
>>> dir(co)
['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
>>> print co.co_names
()
>>> print co.co_name
<module>
>>> print co.co_filename
test.py

创建 pyc 文件

一个 pyc 文件包含三部分独立的信息:

  • magic number
  • pyc 文件的创建时间信息
  • PyCodeObject

import.c

static void
write_compiled_module(PyCodeObject *co, char *cpathname, struct stat *srcstat)
{
    FILE *fp;
    time_t mtime = srcstat->st_mtime;
#ifdef MS_WINDOWS   /* since Windows uses different permissions  */
    mode_t mode = srcstat->st_mode & ~S_IEXEC;
#else
    mode_t mode = srcstat->st_mode & ~S_IXUSR & ~S_IXGRP & ~S_IXOTH;
#endif

    fp = open_exclusive(cpathname, mode);
    if (fp == NULL) {
        if (Py_VerboseFlag)
            PySys_WriteStderr(
                "# can't create %s\n", cpathname);
        return;
    }
    PyMarshal_WriteLongToFile(pyc_magic, fp, Py_MARSHAL_VERSION);     # 写入`magic number`
    /* First write a 0 for mtime */
    PyMarshal_WriteLongToFile(0L, fp, Py_MARSHAL_VERSION);
    PyMarshal_WriteObjectToFile((PyObject *)co, fp, Py_MARSHAL_VERSION);  # 写入`PyCodeObject`
    if (fflush(fp) != 0 || ferror(fp)) {
        if (Py_VerboseFlag)
            PySys_WriteStderr("# can't write %s\n", cpathname);
        /* Don't keep partial file */
        fclose(fp);
        (void) unlink(cpathname);
        return;
    }
    /* Now write the true mtime */
    fseek(fp, 4L, 0);
    assert(mtime < LONG_MAX);
    PyMarshal_WriteLongToFile((long)mtime, fp, Py_MARSHAL_VERSION);   # 写入 pyc 创建时间
    fflush(fp);
    fclose(fp);
    if (Py_VerboseFlag)
        PySys_WriteStderr("# wrote %s\n", cpathname);
}

下面一一进行说明

1,magic number
是 Python 定义的一个整数值,不同版本定义不同,用来确保 Python 的兼容性。Python 在加载 pyc 时首先检查 magic number ,如果与 Python 自身的 magic number 不同,说明创建 pyc 的 Python 版本 与 当前版本不兼容,会拒绝加载。

为什么会不兼容呢?因为字节码指令发生了变化,有删除或增加。

/* Magic word to reject .pyc files generated by other Python versions.
   It should change for each incompatible change to the bytecode.

   The value of CR and LF is incorporated so if you ever read or write
   a .pyc file in text mode the magic number will be wrong; also, the
   Apple MPW compiler swaps their values, botching string constants.

   The magic numbers must be spaced apart atleast 2 values, as the
   -U interpeter flag will cause MAGIC+1 being used. They have been
   odd numbers for some time now.

   There were a variety of old schemes for setting the magic number.
   The current working scheme is to increment the previous value by
   10.

   Known values:
       Python 1.5:   20121
       Python 1.5.1: 20121
       Python 1.5.2: 20121
       Python 1.6:   50428
       Python 2.0:   50823
       Python 2.0.1: 50823
       Python 2.1:   60202
       Python 2.1.1: 60202
       Python 2.1.2: 60202
       Python 2.2:   60717
       Python 2.3a0: 62011
       Python 2.3a0: 62021
       Python 2.3a0: 62011 (!)
       Python 2.4a0: 62041
       Python 2.4a3: 62051
       Python 2.4b1: 62061
       Python 2.5a0: 62071
       Python 2.5a0: 62081 (ast-branch)
       Python 2.5a0: 62091 (with)
       Python 2.5a0: 62092 (changed WITH_CLEANUP opcode)
       Python 2.5b3: 62101 (fix wrong code: for x, in ...)
       Python 2.5b3: 62111 (fix wrong code: x += yield)
       Python 2.5c1: 62121 (fix wrong lnotab with for loops and
                            storing constants that should have been removed)
       Python 2.5c2: 62131 (fix wrong code: for x, in ... in listcomp/genexp)
       Python 2.6a0: 62151 (peephole optimizations and STORE_MAP opcode)
       Python 2.6a1: 62161 (WITH_CLEANUP optimization)
.
*/
#define MAGIC (62161 | ((long)'\r'<<16) | ((long)'\n'<<24))

/* Magic word as global; note that _PyImport_Init() can change the
   value of this global to accommodate for alterations of how the
   compiler works which are enabled by command line switches. */
static long pyc_magic = MAGIC;

2,pyc 创建时间
使得 Python 自动将 pyc 文件与最新的 Python 文件同步。当对 Python 程序进行编译产生 pyc 后,如果后来进行了修改,此时 Python 在尝试加载 pyc 时,会发现 pyc 创建时间早于 Python 程序,于是将重新编译,生成新的 pyc 文件。

3,PyCodeObject
编译器会遍历 PyCodeObject 中的所有域,并依次写入 pyc。对于 PyCodeObject 中的每一个对象,同样会进行遍历,并写入类型标志数据(数值/字符串)

类型标志的三个作用:表明上一个对象的结束、新对象的开始、确定新对象的类型

marshal.h,类型标志

#define TYPE_NULL               '0'
#define TYPE_NONE               'N'
#define TYPE_FALSE              'F'
#define TYPE_TRUE               'T'
#define TYPE_STOPITER           'S'
#define TYPE_ELLIPSIS           '.'
#define TYPE_INT                'i'
#define TYPE_INT64              'I'
#define TYPE_FLOAT              'f'
#define TYPE_BINARY_FLOAT       'g'
#define TYPE_COMPLEX            'x'
#define TYPE_BINARY_COMPLEX     'y'
#define TYPE_LONG               'l'
#define TYPE_STRING             's'
#define TYPE_INTERNED           't'
#define TYPE_STRINGREF          'R'
#define TYPE_TUPLE              '('
#define TYPE_LIST               '['
#define TYPE_DICT               '{'
#define TYPE_CODE               'c'
#define TYPE_UNICODE            'u'
#define TYPE_UNKNOWN            '?'
#define TYPE_SET                '<'
#define TYPE_FROZENSET          '>'

向 pyc 写入字符串

部分略

对于嵌套的名字空间,产生的 PyCodeObject 也是递归嵌套的,嵌套的 PyCodeObject 在上层 PyCodeObject 的co_consts中。

字节码

源代码编译为 字节码指令 序列,虚拟机根据字节码进行操作,完成程序的执行,opcode.h中定义了当前版本 Python 支持的字节码指令。

字节码指令 的编码并不是按顺序增长的,中间有跳跃。

Include目录下的opcode.h定义了字节码指令

#define STOP_CODE    0
#define POP_TOP        1
#define ROT_TWO        2
#define ROT_THREE    3
#define DUP_TOP        4
#define ROT_FOUR    5
#define NOP        9

#define UNARY_POSITIVE    10
#define UNARY_NEGATIVE    11
#define UNARY_NOT    12
#define UNARY_CONVERT    13

#define UNARY_INVERT    15

#define LIST_APPEND    18
#define BINARY_POWER    19

#define BINARY_MULTIPLY    20
#define BINARY_DIVIDE    21
#define BINARY_MODULO    22
#define BINARY_ADD    23
#define BINARY_SUBTRACT    24
#define BINARY_SUBSCR    25
#define BINARY_FLOOR_DIVIDE 26
#define BINARY_TRUE_DIVIDE 27
#define INPLACE_FLOOR_DIVIDE 28
#define INPLACE_TRUE_DIVIDE 29

#define SLICE        30
/* Also uses 31-33 */

#define STORE_SLICE    40
/* Also uses 41-43 */

#define DELETE_SLICE    50
/* Also uses 51-53 */

#define STORE_MAP    54
#define INPLACE_ADD    55
#define INPLACE_SUBTRACT    56
#define INPLACE_MULTIPLY    57
#define INPLACE_DIVIDE    58
#define INPLACE_MODULO    59
#define STORE_SUBSCR    60
#define DELETE_SUBSCR    61

#define BINARY_LSHIFT    62
#define BINARY_RSHIFT    63
#define BINARY_AND    64
#define BINARY_XOR    65
#define BINARY_OR    66
#define INPLACE_POWER    67
#define GET_ITER    68

#define PRINT_EXPR    70
#define PRINT_ITEM    71
#define PRINT_NEWLINE    72
#define PRINT_ITEM_TO   73
#define PRINT_NEWLINE_TO 74
#define INPLACE_LSHIFT    75
#define INPLACE_RSHIFT    76
#define INPLACE_AND    77
#define INPLACE_XOR    78
#define INPLACE_OR    79
#define BREAK_LOOP    80
#define WITH_CLEANUP    81
#define LOAD_LOCALS    82
#define RETURN_VALUE    83
#define IMPORT_STAR    84
#define EXEC_STMT    85
#define YIELD_VALUE    86
#define POP_BLOCK    87
#define END_FINALLY    88
#define BUILD_CLASS    89

#define HAVE_ARGUMENT    90    /* Opcodes from here have an argument: */

#define STORE_NAME    90    /* Index in name list */
#define DELETE_NAME    91    /* "" */
#define UNPACK_SEQUENCE    92    /* Number of sequence items */
#define FOR_ITER    93

#define STORE_ATTR    95    /* Index in name list */
#define DELETE_ATTR    96    /* "" */
#define STORE_GLOBAL    97    /* "" */
#define DELETE_GLOBAL    98    /* "" */
#define DUP_TOPX    99    /* number of items to duplicate */
#define LOAD_CONST    100    /* Index in const list */
#define LOAD_NAME    101    /* Index in name list */
#define BUILD_TUPLE    102    /* Number of tuple items */
#define BUILD_LIST    103    /* Number of list items */
#define BUILD_MAP    104    /* Always zero for now */
#define LOAD_ATTR    105    /* Index in name list */
#define COMPARE_OP    106    /* Comparison operator */
#define IMPORT_NAME    107    /* Index in name list */
#define IMPORT_FROM    108    /* Index in name list */

#define JUMP_FORWARD    110    /* Number of bytes to skip */
#define JUMP_IF_FALSE    111    /* "" */
#define JUMP_IF_TRUE    112    /* "" */
#define JUMP_ABSOLUTE    113    /* Target byte offset from beginning of code */

#define LOAD_GLOBAL    116    /* Index in name list */

#define CONTINUE_LOOP    119    /* Start of loop (absolute) */
#define SETUP_LOOP    120    /* Target address (relative) */
#define SETUP_EXCEPT    121    /* "" */
#define SETUP_FINALLY    122    /* "" */

#define LOAD_FAST    124    /* Local variable number */
#define STORE_FAST    125    /* Local variable number */
#define DELETE_FAST    126    /* Local variable number */

#define RAISE_VARARGS    130    /* Number of raise arguments (1, 2 or 3) */
/* CALL_FUNCTION_XXX opcodes defined below depend on this definition */
#define CALL_FUNCTION    131    /* #args + (#kwargs<<8) */
#define MAKE_FUNCTION    132    /* #defaults */
#define BUILD_SLICE     133    /* Number of items */

#define MAKE_CLOSURE    134     /* #free vars */
#define LOAD_CLOSURE    135     /* Load free variable from closure */
#define LOAD_DEREF      136     /* Load and dereference from closure cell */ 
#define STORE_DEREF     137     /* Store into cell */ 

/* The next 3 opcodes must be contiguous and satisfy
   (CALL_FUNCTION_VAR - CALL_FUNCTION) & 3 == 1  */
#define CALL_FUNCTION_VAR          140    /* #args + (#kwargs<<8) */
#define CALL_FUNCTION_KW           141    /* #args + (#kwargs<<8) */
#define CALL_FUNCTION_VAR_KW       142    /* #args + (#kwargs<<8) */

/* Support for opargs more than 16 bits long */
#define EXTENDED_ARG  143

解析 pyc

由于包含嵌套 PyCodeObject,pyc 中的二进制数据实际上是有结构的,可以以 XML格式进行解析,从而可视化。使用 pycparser。

而 Python 库中 dis 的 dis 方法可以对 code对象 进行解析。接收 code对象,输出 字节码指令信息。

dis.dis 的输出:

  • 第一列,是 字节码指令 对应的 源代码 在 Python 程序中的行数
  • 第二列,是当前 字节码指令 在 co_code 中的偏移位置
  • 第三列,当前的字节码指令
  • 第四列,当前字节码指令的参数

test.py

import sys

a = 1

def b():
    print a
    a = 2
    print a

>>> source = open('/Users/chao/Desktop/test.py').read()
>>> co = compile(source, 'test.py', 'exec')
>>> import dis
>>> dis.dis(co)
  1           0 LOAD_CONST               0 (-1)
              3 LOAD_CONST               1 (None)
              6 IMPORT_NAME              0 (sys)
              9 STORE_NAME               0 (sys)

  3          12 LOAD_CONST               2 (1)
             15 STORE_NAME               1 (a)

  5          18 LOAD_CONST               3 (<code object b at 0x1005dc930, file "test.py", line 5>)
             21 MAKE_FUNCTION            0
             24 STORE_NAME               2 (b)
             27 LOAD_CONST               1 (None)
             30 RETURN_VALUE
>>> type(co)
<type 'code'>
>>> dir(co)
['__class__', '__cmp__', '__delattr__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'co_argcount', 'co_cellvars', 'co_code', 'co_consts', 'co_filename', 'co_firstlineno', 'co_flags', 'co_freevars', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames']
>>> print co.co_names
('sys', 'a', 'b')
>>> print co.co_name
<module>
>>> print co.co_filename
test.py

参考资料

《Python 源码剖析》第七章

评论关闭