从学编程开始老师就教我们Java是一门编译型语言。而《C#高级编程》中却没有说C#是一门编译型语言。我不知道编译型和解释型语言的明确区别,但是不论是Python还是Java、C#,他们的工作流程都一样,先编译撑一个中间语言,然后交给虚拟机/运行时/解释器来执行命令。Python的编译结果就是今天要讲的pyc文件。pyc文件经常让开发者忽略,而且十分讨厌。
Python的项目中总是带有一大堆的pyc文件,即使你删除了,你重新运行一下项目,他们还是会生成。但是很少有人知道什么时候会生成pyc。python a.py这样是不会生成a.pyc的,pyc文件的生成是通过import来触发的,也就是说,import a这样才会生成pyc文件。如果想知道pyc文件中存了什么,首先必须知道python的工作机制。
普通的python代码是不会运行的,必须编译成字节码,然后把字节码交给python解释器(python虚拟机),python虚拟机把这些字节码翻译成CPU指令。而pyc文件中的就是字节码和一些静态数据。
PyCodeObject对象就是Python编译器编译完py文件后在内存中的对象。PyCodeObject写入文件后就形成了pyc文件。所以PyCodeObject才是真正的编译结果。
typedef struct { PyObject_HEAD int co_argcount; // arg的数量 int co_nlocals; // 局部变量的数量 int co_stacksize; // 需要用到的栈的空间 int co_flags; // 不知道干嘛用的 PyObject *co_code; // 字节码指令序列,以PyStringObject的形式存在 PyObject *co_consts; // PyTupleObject对象,保存这个PyCodeObject中的常量 PyObject *co_names; // PyTupleObject 变量名 PyObject *co_varnames; // 局部变量的变量名 PyObject *co_freevars; // 闭包 PyObject *co_cellvars; // 嵌套函数所引用的局部变量名集合 PyObject *co_filename; // 对应的文件的完整路径 PyObject *co_name // 命名空间的名字 int co_firstlineno; // 对应的源文件的这个PyCodeObject的第一行 PyObject *co_lnotab; // 字节码与行号的对应关系 void *co_zombieframe; // } PyCodeObject;
PyCodeObject对应的是一个命名空间的代码块,所以一个py文件可以生成很多个PyCodeObject对象,这些PyCodeObject对象是以与代码相应的嵌套结构存放在一个pyc文件中。而pyc中存放的不仅仅是PyCodeObject对象,还存放了magic number和pyc文件的创建时间。magic number是用来确定python的版本的一个特殊数字,pyc创建时间是用来确定是否需要重新生成pyc文件。
PyCodeObject写入文件主要通过了两个基本的方法,w_long(longx, WFILE p)和w_string(char s, int n, WFILE p)方法,而python源码又将这些两个方法组合撑一个对外的方法w_object(PyObject v, WFILE *p)方法。在写入一个变量之前,总是会在前面先写入这个变量的类型,例如TYPE_INT,TYPE_LIST, TYPE_CODE等,这些类型标识以及标识对应的简写符号全部放在import.c文件中。Python在向pyc文件写入字符串的时候比较特殊,要注意interned机制(在pyc中也存在interned机制)。
查看PyCodeObject对象的方法。
>>> f = open("tt.py").read() >>> co = compile(f, "tt.py", "exec") >>> dir(co) >>> co.co_code
另外也能查看一个CodeObjectBlock对应的字节码。
In [8]: import a
"hi"
In [9]: import dis
In [10]: dis.dis(a)
Disassembly of say:
5 0 LOAD_CONST 1 ('hello world')
3 PRINT_ITEM
4 PRINT_NEWLINE
5 LOAD_CONST 0 (None)
8 RETURN_VALUE