运行程序

当在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_compile和compile可以帮助手动产生 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)
>>> 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
>>> 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 ()
21 MAKE_FUNCTION 0
24 STORE_NAME 2 (b)
27 LOAD_CONST 1 (None)
30 RETURN_VALUE
>>> type(co)
>>> 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
>>> print co.co_filename
test.py

参考资料

《Python 源码剖析》第七章