一棒拂、Python程序執(zhí)行原理
1.一個小程序
# [demo.py]
class A(object):
pass
def func():
a = 5
b = 2
print 'hello coco!'
a = A()
func()
對于如上一個簡單程序,稍有python編程經(jīng)驗都能理輕松理解玫氢。執(zhí)行指令:
python demo.py
如我們預(yù)期帚屉,程序會產(chǎn)生執(zhí)行結(jié)果:
hello coco!
2.執(zhí)行流程
如上所示,一個文本文件demo.py漾峡,經(jīng)過python施加魔法后變成機器指令并執(zhí)行起來攻旦。那么python內(nèi)部究竟是如何運作呢?如我們所知生逸,python是一門解釋性語言牢屋,它的執(zhí)行流程與Java且预、C#這些解釋型語言一樣可以用兩個詞概括編譯、解析烙无。
1)編譯
對于上述的python程序锋谐,執(zhí)行后細(xì)心的同學(xué)會發(fā)現(xiàn)程序文件夾多了一個demo.pyc文件。事實上皱炉,這個pyc文件就是對demo.py文件的編譯結(jié)果。編譯結(jié)果是一個稱之為python字節(jié)碼序列(為運行時Python虛擬機所執(zhí)行的指令)狮鸭。python字節(jié)碼與機器指令碼很相似:
12 MAKE_FUNCTION 0
15 CALL_FUNCTION 0
18 BUILD_CLASS
19 STORE_NAME
先把python文件編譯成字節(jié)碼主要有兩個好處合搅,第一方面也是最重要的:跨平臺是python的一大特性,不同機器的機器指令是不同的歧蕉,將代碼先編譯為python解釋器識別的字節(jié)碼灾部,運行時可以根據(jù)不同機器執(zhí)行相應(yīng)的機器指令;第二方面惯退,將python先編譯成字節(jié)碼可以提升運行性能赌髓,將編譯內(nèi)容事先存儲到pyc文件中(pyc文件中不單存儲了字節(jié)碼信息),如無修改運行時無需再次編譯催跪。
2)解釋
python解釋器首先將python文件編譯為字節(jié)碼锁蠕,解釋為字節(jié)碼后python的解析器-python虛擬機就開始接手所有的工作:依次讀入每條字節(jié)碼指令并逐條執(zhí)行。
二懊蒸、PyCodeObject
如上我們大概了解了python的執(zhí)行流程以及python字節(jié)碼的概念荣倾,接下來將深入源碼探索python實現(xiàn)這些機制的內(nèi)部細(xì)節(jié)。首先看python的編譯過程骑丸,如前說到python將編譯結(jié)果字節(jié)碼存儲到pyc文件中舌仍,事實上這個結(jié)論還不夠全面。pyc中除了字節(jié)碼還存儲了很多python程序運行時信息包括定義的字符串通危、常量等铸豁。python的編譯結(jié)果的的奧秘全部藏在PyCodeObject中,PyCodeObject是python中的一個命名空間(命名空間指的是有獨立變量定義的Code block如函數(shù)菊碟、類节芥、模塊等)的編譯結(jié)果在內(nèi)存中的表示。
/* 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等字段逆害,這些字段的內(nèi)容如下表:
- co_nlocals : Code Block中局部變量個數(shù)藏古,包括其位置參數(shù)個數(shù)
- co_stacksize : 執(zhí)行該段Code Block需要的棧空間
- co_code : Code Block編譯得到的字節(jié)碼指令序列忍燥,以PyStringObject的形式存在
- co_consts: PyTupleObject拧晕,保存Code Block中的所有常量
- co_names: PyTupleObject, 保存Code Block中的所有符號
- co_varnames: Code Block中的局部變量名集合
- co_freevars : Python實現(xiàn)閉包存儲內(nèi)容
- co_cellvars : Code Block中內(nèi)部嵌套函數(shù)所引用的局部變量名集合
- co_filename : Code Block對應(yīng)的.py文件的完整路徑
- co_name : Code Block的名字,通常是函數(shù)名或類名
python層的code對象與PyCodeObject對應(yīng)梅垄,通過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中不僅存儲了代碼對應(yīng)的字節(jié)碼指令序列,還保存了代碼的運行時信息靡馁。PyCodeObject是這些信息在內(nèi)存中的表示秧饮,為以后執(zhí)行能重復(fù)利用這些編譯信息,減少編譯時間(在代碼未改變的情況下)浅缸,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中實現(xiàn):
/*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字節(jié)碼
在opcode.h中定義了python的字節(jié)碼指令定義结缚。
/* 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對應(yīng)的字節(jié)碼指令:
>>> 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