阅读条件:

C语言基础, Python爱好者


前言

前面我们已经谈到过python面向对象的实现.今天我们来看看Python虚拟机的基础.

主要内容分为两个部分:

  1. Python源码编译后的结果: .pyc文件和PyCodeObject对象.
  2. Python的模拟函数栈帧: PyFrameObject对象.

编译Python源码

大多时候,我们运行python代码都不会关注源码的编译结果.因为python虚拟机是将编译和运行两部分合二为一了.

而另一种常见的解释型语言java,则将两部分分开,分别是: javacjava.我们通过javac命令来生成java源码对应的class文件,再通过java命令来运行.

事实上,python也可以达到同样的结果.我们通过以下的命令可以生成.pyc文件.

1
python -m py_compile <source_code.py>

事实上,.pyc和class文件应该基本类似的.

pyc文件的内容

pyc文件主要由三部分内容组成:

  1. magic number

主要标识了Python的版本,不同版本的python所支持的指令是不同的.所以Python2.7编译而成的pyc文件是不能由Python2.5去运行的.

  1. 编译时间

这个主要用于决定是否重新编译pyc文件.如果源码的更新时间晚于pyc的生成时间,那么肯定是要重新编译的.

  1. 编译结果

这个是真正的戏肉.是源码对应的指令和一系列信息的集合.是PyCodeObject对象序列化的结果.

PyCodeObject对象

Python是一门解释型语言,它对源码进行了编译.本质上,就是将源码中的信息提取出来,按照它的规则来重新整理. 那么它生成的结果在内存中的体现就是PyCodeObject.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[Include/code.h]
/* Bytecode object */
typedef struct {
    PyObject_HEAD
    int co_argcount;		/* 一般参数的个数,不包括扩展参数 */
    int co_nlocals;		/* 局部变量的个数 */
    int co_stacksize;		/* 运行所需要的栈大小 */
    int co_flags;		/* 一些标志位: 比如是否是生成器,是否有扩展参数之类的 */
    PyObject *co_code;		/* Python指令 */
    PyObject *co_consts;	/* 常量对象 */
    PyObject *co_names;		/* 名字字符串 */
    PyObject *co_varnames;	/* 参数变量名元组 */
    PyObject *co_freevars;	/* 闭包相关: 引用外部变量名 */
    PyObject *co_cellvars;      /* 闭包相关: 被内部嵌套函数引用的变量名 */
    /* The rest doesn't count for hash/cmp */
    PyObject *co_filename;	/* 文件名 */
    PyObject *co_name;		/* string (name, for reference) */
    int co_firstlineno;		/* 指令首行位置 */
    PyObject *co_lnotab;	/* string (encoding addr<->lineno mapping) */
    void *co_zombieframe;     /* for optimization only (see frameobject.c) */
} PyCodeObject;

其中需要重点关注的是co_constsco_names. python编译而成的那些对象保存在co_consts中,尤其其中还可以嵌套其他的PyCodeObject. 局部变量名保存在co_names中,事实上在执行基本指令时,这两个域使用的相当频繁.

而pyc文件就是将PyCodeObject保存在文件中的结果.执行的时候,还是会将pyc文件恢复成PyCodeObject.

模拟函数运行栈帧

有了PyCodeObject,我们就有了需要运行的对象.但是我们还需要模拟函数运行时的栈帧.这个工作由PyFrameObject来承担. 也可以说对于一段代码而言,PyCodeObject是其静态部分,而PyFrameObject则是其动态部分.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[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 */
    /* Next free slot in f_valuestack.  Frame creation sets to f_valuestack.
       Frame evaluation usually NULLs it, but a frame that yields sets it
       to the current stack top. */
    PyObject **f_stacktop;
    PyObject *f_trace;		/* Trace function */

    /* If an exception is raised in this frame, the next three are used to
     * record the exception info (if any) originally in the thread state.  See
     * comments before set_exc_info() -- it's not obvious.
     * Invariant:  if _type is NULL, then so are _value and _traceback.
     * Desired invariant:  all three are NULL, or all three are non-NULL.  That
     * one isn't currently true, but "should be".
     */
    PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;

    PyThreadState *f_tstate;
    int f_lasti;		/* Last instruction if called */
    /* As of 2.3 f_lineno is only valid when tracing is active (i.e. when
       f_trace is set) -- at other times use PyCode_Addr2Line instead. */
    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;

其中有些很有趣的域.

  1. f_back: 表示上一帧.也将各个栈帧串联了起来.
  2. f_code: 关联PyCodeObject
  3. f_builtins, f_globals, f_locals: 则表示不同的namespace.还记得LEGB规则么?这就是他们在代码中的实现.
  4. f_valuestack, f_stacktop: 表示栈顶和栈底.
  5. f_tstate: 表示线程状态
  6. f_blockstack: 实现了循环,异常等机制.
  7. f_localplus: 参数保存的域: 包括函数调用时传递的各种参数.局部参数.闭包相关的参数.

一言难尽,还是自己去读一读<Python源码剖析>.还是挺精彩的.

执行入口

我们有了要执行的代码PyCodeObject,我们有了函数栈帧PyFrameObject.那么谁来执行呢? 答案是:Python/ceval.c PyEval_EvalFrameEx. 是它从PyCodeObject取出一条条指令,在PyFrameObject中执行.

总结

Python运行是在PyFrameObject中.主要是PyCodeObject中的co_constsco_names域和PyFrameObject中的临时栈和f_locals进行互动.

我知道这部分写的太过简略,这只是我个人的总结.更多的还是要去看书.