目录

    文中以 Python 2.7.8 版本源码为例。

    1. Python 中常见的文件格式

    • py 文件

      Python 源代码文件,可以使用文本编辑器进行修改。

    • pyc 文件

      Python 源代码编译后,生成的字节码文件。

    • pyw 文件

      pyc 文件执行时,会出现 console 窗口;pyw 文件执行时,不会出现。pyw 文件主要是用来运行纯 GUI 图形用户界面程序,运行时,需要 Pythonw 解释执行。

    • pyo 文件

      Python 源代码优化编译后的文件。 执行命令,python -O your.py, 即可将 Python 源代码编译为 pyo 文件

    • pyd 文件

    一般是其他语言编写的 Python 扩展模块。pyd 文件是用 D 语言(C/C++综合进化版本)编写,编译生成的二进制文件。

    2. py 生成 pyc 文件

    如果对代码目录具有写的权限,在两种情况下,py 源代码文件会编译生成 pyc 文件:

    • 被 import 的模块,会生成 pyc 文件。
    • py_compile 编译的 Python 代码会生成 pyc 文件

    需要知道的是,直接执行 py 源代码文件,并不会自动生成 pyc 文件。

    单文件编译:

    import py_compile
    py_compile.compile(r'./your.py')
    

    文件夹编译:

    import compileall
    compileall.compile_dir('./')
    

    pyc 是由 py 源代码文件经过编译后生成的,一种跨平台的二进制字节码文件。Python 解释器能解释执行 pyc 文件,类似于 Java 虚拟机。同时,pyc 的内容与 Python 的版本相关,不同版本的 Python 解释器,编译 py 源代码后生成的 pyc 文件不同。Python 2.7 编译生成的 pyc,Python 3.5 是无法执行的。

    如果跨版本执行 pyc,可能会报错(这里说可能,是因为小版本之间的 magic number 相同,可以执行):

    RuntimeError: Bad magic number in .pyc file
    

    在下面的内容中,还会有进一步的解释。

    3. import 实现的源码

    Python 中 import 会生成 pyc 文件,下面从 Python 源码中 import 实现部分开始分析。

    • 源码位置:Python\import.c
    /* Load a source module from a given file and return its module */
    /* object WITH INCREMENTED REFERENCE COUNT.  If there's a matching */
    /* byte-compiled file, use that instead. */
    static PyObject *
    load_source_module(char *name, char *pathname, FILE *fp)
    {
        if (check_compiled_module(pathname, mtime, cpathname))) {
            co = read_compiled_module(cpathname, fpc);
        }
        else {
            write_compiled_module(co, cpathname, &st, mtime);
        }
        m = PyImport_ExecCodeModuleEx(name, (PyObject *)co, pathname);
    }
    

    Python 执行 import 指令时,在代码目录下查找同名,后缀为 pyw、pyo、pyc 的文件。如果找到,则调用 check_compiled_module 函数,判断 pyc 文件中,头部的时间戳,是否与 py 文件 最后修改时间 一致。如果一致则,直接加载;否则重新编译生成 pyc 文件,并将 最后修改时间 写入 py 源码。 如果没找到,则编译生成 pyc 文件。下面是 check_compiled_module 的源码。

    • 源码位置:Python\import.c
    /* Given a pathname for a Python source file, its time of last */
    /* modification, and a pathname for a compiled file, check whether the */
    /* compiled file represents the same version of the source.  If so, */
    /* return a FILE pointer for the compiled file, positioned just after */
    /* the header; if not, return NULL. */
    /* Doesn't set an exception. */
    static FILE *
    check_compiled_module(char *pathname, time_t mtime, char *cpathname)
    {
        ...
        pyc_mtime = PyMarshal_ReadLongFromFile(fp);
        if (pyc_mtime != mtime) {
            if (Py_VerboseFlag)
                PySys_WriteStderr("# %s has bad mtime\n", cpathname);
            fclose(fp);
            return NULL;
        }
        ...
    }
    

    check_compiled_module 函数是通过检查 py 和 pyc 文件的最后修改的时间戳 mtime 是否一致,来判断是否需要更新 pyc 文件。

    • 源码位置:Python\import.c
    /* Write a compiled module to a file, placing the time of last*/
    /* modification of its source into the header.*/
    /* Errors are ignored, if a write error occurs an attempt is made to*/
    /* remove the file. */
    static void
    write_compiled_module(PyCodeObject *co, char *cpathname, struct stat *srcstat, time_t mtime)
    {
        ...
        PyMarshal_WriteLongToFile(pyc_magic, fp, Py_MARSHAL_VERSION);
        /* First write a 0 for mtime */
        PyMarshal_WriteLongToFile(0L, fp, Py_MARSHAL_VERSION);
        PyMarshal_WriteObjectToFile((PyObject *)co, fp, Py_MARSHAL_VERSION);
        ...
        /* Now write the true mtime (as a 32-bit field) */
        fseek(fp, 4L, 0);
        assert(mtime <= 0xFFFFFFFF);
        PyMarshal_WriteLongToFile((long)mtime, fp, Py_MARSHAL_VERSION);
        fflush(fp);
        fclose(fp);
        ...
    }
    

    在写入 pyc 文件时,还需要写入一个 Long 型变量,变量的内容则是 py 源文件的最后修改时间戳 ftLastWriteTime

    4. 不同版本的 pyc 如何区分

    前面提到,不同 Python 版本的解释器生成的 pyc 文件不一样。那么,Python 解释器是如何加以区分的呢?

    在上面生成 pyc 文件的源码中,可以看到 PyMarshal_WriteLongToFile(pyc_magic, fp, Py_MARSHAL_VERSION);pyc_magicPy_MARSHAL_VERSION 变量写入 pyc 文件。Py_MARSHAL_VERSION 指定了当前的文件格式,2.7.8 版本为 2,3.5.5 版本为 4;pyc_magic 指定了和 Python 解释器相关的版本信息。

    • 源码位置:Python\import.c
    /* Magic word to reject .pyc files generated by other Python versions.
       It should change for each incompatible change to the bytecode.
    
       Known values:
           ...
           Python 2.6a0: 62151 (peephole optimizations and STORE_MAP opcode)
           Python 2.6a1: 62161 (WITH_CLEANUP optimization)
           Python 2.7a0: 62171 (optimize list comprehensions/change LIST_APPEND)
           Python 2.7a0: 62181 (optimize conditional branches:
                    introduce POP_JUMP_IF_FALSE and POP_JUMP_IF_TRUE)
           Python 2.7a0  62191 (introduce SETUP_WITH)
           Python 2.7a0  62201 (introduce BUILD_SET)
           Python 2.7a0  62211 (introduce MAP_ADD and SET_ADD)
    */
    #define MAGIC (62211 | ((long)'\r'<<16) | ((long)'\n'<<24))
    static long pyc_magic = MAGIC;
    

    可以看到,在不同的 Python 解释器版本中 pyc_magic 值不同。通过定义新的 pyc_magic 可以用于生成专属的 Python 解释器,这也是实现 pyc 文件和指定 Python 解释器绑定的一种方式。

    5. PyCodeObject & PyFrameObject

    PyCodeObject 是 Python 源代码编译后,真正生成的结果。也就是说,编写的 Python 源代码都会被转换成 PyCodeObject 对象。

    源码位置: Include\code.h

    /* Bytecode object */
    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;
    

    PyCodeObjec 对象的 co_code 中保存着字节码指令。Python 解释器,执行字节码指令序列的过程就是,从头到尾遍历整个 co_code 依次执行字节码指令的过程。

    源码位置:Python\marshal.c

    void
    PyMarshal_WriteObjectToFile(PyObject *x, FILE *fp, int version)
    {
        WFILE wf;
        wf.fp = fp;
        wf.error = WFERR_OK;
        wf.depth = 0;
        wf.strings = (version > 0) ? PyDict_New() : NULL;
        wf.version = version;
        w_object(x, &wf);
        Py_XDECREF(wf.strings);
    }
    

    PyMarshal_WriteObjectToFile 写入 PyCodeObject 对象到 pyc 文件,函数内部调用 w_object 将 Python 代码中出现的对象和对应的 TYPE_* 标识一一存入对象,比如int对象对应标识 TYPE_INT。

    但是,Python 程序在运行时,它的解释器上处理的并不是一个 PyCodeObject对象,而是与之对应的帧栈对象 PyFrameObject。

    • 源码位置:Include\frameobject.h
    typedef struct _frame {
        PyObject_VAR_HEAD
        struct _frame *f_back;    /* previous frame, or NULL */
        PyCodeObject *f_code;    /* code segment */
        PyObject *f_builtins;    /* builtin symbol table (PyDictObject) */
        PyObject *f_globals;    /* global symbol table (PyDictObject) */
        PyObject *f_locals;        /* local symbol table (any mapping) */
        PyObject **f_valuestack;    /* points after the last local */
        PyObject **f_stacktop;
        PyObject *f_trace;        /* Trace function */
        PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;
        PyThreadState *f_tstate;
        int f_lasti;        /* Last instruction if called */
        int f_lineno;        /* Current line number */
        int f_iblock;        /* index in f_blockstack */
        PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */
        PyObject *f_localsplus[1];    /* locals+stack, dynamically sized */
    } PyFrameObject;
    

    Python 解释器,用 PyInterpreterState 结构维护进程运行环境,PyThreadState 维护线程运行环境,PyFrameObject 维护栈帧运行环境,三者是依次包含关系,如下图所示:

    Python 解释器,动态加载上述三种结构进内存,并模拟操作系统执行过程。程序执行后,先创建各个运行时环境,再将栈帧中的字节码载入,循环遍历解释执行。

    6. 参考