五、PyCodeObject与Python程序执行

一、Python程序执行原理

1.一个小程序

# [demo.py]
class A(object):
  pass

def func():
  a = 5
  b = 2
  print 'hello coco!'

a = A()
func()

对于如上一个简单程序,稍有python编程经验都能理轻松理解。执行指令:

python demo.py

如我们预期,程序会产生执行结果:

hello coco!

2.执行流程

如上所示,一个文本文件demo.py,经过python施加魔法后变成机器指令并执行起来。那么python内部究竟是如何运作呢?如我们所知,python是一门解释性语言,它的执行流程与Java、C#这些解释型语言一样可以用两个词概括编译、解析。

1)编译

对于上述的python程序,执行后细心的同学会发现程序文件夹多了一个demo.pyc文件。事实上,这个pyc文件就是对demo.py文件的编译结果。编译结果是一个称之为python字节码序列(为运行时Python虚拟机所执行的指令)。python字节码与机器指令码很相似:

12 MAKE_FUNCTION            0
15 CALL_FUNCTION            0
18 BUILD_CLASS         
19 STORE_NAME   

先把python文件编译成字节码主要有两个好处,第一方面也是最重要的:跨平台是python的一大特性,不同机器的机器指令是不同的,将代码先编译为python解释器识别的字节码,运行时可以根据不同机器执行相应的机器指令;第二方面,将python先编译成字节码可以提升运行性能,将编译内容事先存储到pyc文件中(pyc文件中不单存储了字节码信息),如无修改运行时无需再次编译。

2)解释

python解释器首先将python文件编译为字节码,解释为字节码后python的解析器-python虚拟机就开始接手所有的工作:依次读入每条字节码指令并逐条执行。

二、PyCodeObject

如上我们大概了解了python的执行流程以及python字节码的概念,接下来将深入源码探索python实现这些机制的内部细节。首先看python的编译过程,如前说到python将编译结果字节码存储到pyc文件中,事实上这个结论还不够全面。pyc中除了字节码还存储了很多python程序运行时信息包括定义的字符串、常量等。python的编译结果的的奥秘全部藏在PyCodeObject中,PyCodeObject是python中的一个命名空间(命名空间指的是有独立变量定义的Code block如函数、类、模块等)的编译结果在内存中的表示。

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

从源码中可以看到PyCodeObject中包含了co_argcount和co_nlocals等字段,这些字段的内容如下表:

  • co_nlocals : Code Block中局部变量个数,包括其位置参数个数
  • co_stacksize : 执行该段Code Block需要的栈空间
  • co_code : Code Block编译得到的字节码指令序列,以PyStringObject的形式存在
  • co_consts: PyTupleObject,保存Code Block中的所有常量
  • co_names: PyTupleObject, 保存Code Block中的所有符号
  • co_varnames: Code Block中的局部变量名集合
  • co_freevars : Python实现闭包存储内容
  • co_cellvars : Code Block中内部嵌套函数所引用的局部变量名集合
  • co_filename : Code Block对应的.py文件的完整路径
  • co_name : Code Block的名字,通常是函数名或类名

python层的code对象与PyCodeObject对应,通过python的compile接口可以查看code对象(即PyCodeObject对象)。

>>> source = open('demo.py').read()
>>> co = compile(source,'demo.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']
>>> co.co_names
('object', 'A', 'func', 'a')
>>> co.co_filename
'demo.py'

三、持久化PyCodeObject

PyCodeObject中不仅存储了代码对应的字节码指令序列,还保存了代码的运行时信息。PyCodeObject是这些信息在内存中的表示,为以后执行能重复利用这些编译信息,减少编译时间(在代码未改变的情况下),python解释器会把这些信息序列化到pyc文件中。

/*import.c*/
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);
}

pyc文件中会记录把编译的版本号,编译时间、PyCodeObject等信息序列化到pyc文件中,实际的序列化过程在marshal.c中实现:

/*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);
}

static void w_object(PyObject *v, WFILE *p)
{
    Py_ssize_t i, n;

    p->depth++;

    if (p->depth > MAX_MARSHAL_STACK_DEPTH) {
        p->error = WFERR_NESTEDTOODEEP;
    }
    else if (v == NULL) {
        w_byte(TYPE_NULL, p);
    }
    else if ...

四、Python字节码

在opcode.h中定义了python的字节码指令定义。

/* opcode.h*/
/* Instruction opcodes for compiled code */
#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_SET   104     /* Number of set items */
#define BUILD_MAP   105 /* Always zero for now */
#define LOAD_ATTR   106 /* Index in name list */

python中也提供了dis工具可以查看PyCodeObject对应的字节码指令:

>>> source = open('demo.py').read()
>>> co = compile(source, 'demo.py', 'exec')
>>> import dis
>>> dis.dis(co)
  1           0 LOAD_CONST               0 ('A')
              3 LOAD_NAME                0 (object)
              6 BUILD_TUPLE              1
              9 LOAD_CONST               1 (<code object A at 0x7f993a73adb0, file "demo.py", line 1>)
             12 MAKE_FUNCTION            0
             15 CALL_FUNCTION            0
             18 BUILD_CLASS         
             19 STORE_NAME               1 (A)

  4          22 LOAD_CONST               2 (<code object func at 0x7f993a662a30, file "demo.py", line 4>)
             25 MAKE_FUNCTION            0
             28 STORE_NAME               2 (func)

  9          31 LOAD_NAME                1 (A)
             34 CALL_FUNCTION            0
             37 STORE_NAME               3 (a)

 10          40 LOAD_NAME                2 (func)
             43 CALL_FUNCTION            0
             46 POP_TOP             
             47 LOAD_CONST               3 (None)
             50 RETURN_VALUE  

推荐阅读更多精彩内容