文章目录
  1. 1. 前言
  2. 2. magic number 和修改时间
  3. 3. PyCodeObject 对象
  4. 4. 查看字节码

前言

Python 是一门解释性语言,源码执行需要经过:编译-字节码-虚拟机的步骤。本文就介绍一下 .py 文件编译后的 .pyc 文件结构。直接运行的代码不会生成 .pyc,而 Python 的 import 机制会触发 .pyc 文件的生成。

magic number 和修改时间

我们在导入模块的源码中,能找到 .pyc 文件的蛛丝马迹:

1
2
3
4
5
6
7
8
9
10
11
// Python/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 文件包含三个主要内容:magic number,修改时间,和一个 PyCodeObject 对象。

magic number 和 Python 版本有关,magic number 不同能够防止低版本误执行高版本编译后的字节码。修改时间能让编译器决定是否重新编译源文件。

我们来读取 .pyc 的前八字节来验证一下我们的分析,func0.py 是测试脚本:

1
2
3
4
5
6
7
## func0.py
f = open(fname, 'rb')
magic = f.read(4)
moddate = f.read(4)
modtime = time.asctime(time.localtime(struct.unpack('I', moddate)[0]))
print 'magic number: %s' % (magic.encode('hex'))
print 'moddate %s (%s)' % (moddate.encode('hex'), modtime)

被测试源文件:

1
2
3
4
# test.py
def add(a):
a=1
print a

使用以下强制编译成 .pyc

1
python -m py_compile test.py

测试结果:

1
2
3
$ python func0.py test.pyc
magic number: 03f30d0a
moddate 5e9a4b5a (Tue Jan 2 22:42:38 2018)

PyCodeObject 对象

PyCodeObject 是一段代码块编译的直接结果。换句话说,一个作用域,对应一个代码,最终对应一个编译后的 PyCodeObject
首先看一下 PyCodeObject 的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;

一段程序不可能只有一个作用域,嵌套的子作用域存在于 co_consts 之中。

我们写个简单的脚本,展现这种嵌套的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# func0.py
...
def show_code(code, indent=''):
old_indent = indent
print "%s<code>" % indent
indent += ' '
show_hex("bytecode", code.co_code, indent=indent)
print "%s<filename> %r</filename>" % (indent, code.co_filename)
print "%s<consts>" % indent
for const in code.co_consts:
if type(const) == types.CodeType:
show_code(const, indent+' ')
else:
print " %s%r" % (indent, const)
print "%s</consts>" % indent
print "%s</code>" % old_indent

被测试的源文件:

1
2
3
4
# test.py
def add(a):
a=1
print a

测试步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
exs $ python func0.py test.pyc
<code>
<bytecode> 6400008400005a000064010053</bytecode>
<filename> 'test.py'</filename>
<consts>
<code>
<bytecode> 6401007d00007c0000474864000053</bytecode>
<filename> 'test.py'</filename>
<consts>
None
</consts>
</code>
None
</consts>
</code>

可以看到,函数 add 作为全局作用域的中一个子作用域,在编译结果中,是以常量形式存在于全局作用域的 PyCodeObject 中。

查看字节码

Python 提供了 dis 模块,其中的 disassemble() 函数可以反编译 PyCodeObject 对象,以可读的形式展现出来。
我们修改 func0.py,将字节码对应的指令打印出来,增加下述代码:

1
2
3
4
5
6
...
show_hex("bytecode", code.co_code, indent=indent)
print "%s<dis>" % indent
dis.disassemble(code)
print "%s</dis>" % indent
...

测试结果:

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
$ python func0.py test.pyc
<code>
<bytecode> 6400008400005a000064010053</bytecode>
<dis>
3 0 LOAD_CONST 0 (<code object add at 0x109aa1c30, file "test.py", line 3>)
3 MAKE_FUNCTION 0
6 STORE_NAME 0 (add)
9 LOAD_CONST 1 (None)
12 RETURN_VALUE
</dis>
<filename> 'test.py'</filename>
<consts>
<code>
<bytecode> 6401007d00007c0000474864000053</bytecode>
<dis>
4 0 LOAD_CONST 1 (1)
3 STORE_FAST 0 (a)

5 6 LOAD_FAST 0 (a)
9 PRINT_ITEM
10 PRINT_NEWLINE
11 LOAD_CONST 0 (None)
14 RETURN_VALUE
</dis>
<filename> 'test.py'</filename>
<consts>
None
1
</consts>
</code>
None
</consts>
</code>
文章目录
  1. 1. 前言
  2. 2. magic number 和修改时间
  3. 3. PyCodeObject 对象
  4. 4. 查看字节码