Python之code对象与pyc文件(一)
Python程序的执行过程
我们都知道,C语言在执行之前需要将源代码编译成可执行的二进制文件,也就是将源代码翻译成机器代码,这种二进制文件一旦生成,即可用于执行。但是,Python是否一样呢?或许很多人都听过,Python和Java都是半编译半解释的语言,那么问题来了,什么又是半编译半解释呢?这还要从C语言开始说起
比方我们现在有一段C语言写成的程序,我们在一台Linux服务器上编译好了,生成可执行的二进制文件,可是我现在想要在一台Windows的机器上执行这个文件,这是不可能的,原因是因为不同平台间的机器代码是不一样的,在Linux机器上生成的二进制可执行文件,是不能拿到Windows上执行的,甚至都是在Linux上编译的文件,但是用的C编译器不同,一样有可能无法执行。所以,这才有了半编译半解释。
半编译半解释保证,一次编译,到处运行。这是Java的承诺,同样适用于Python。半编译半解释会从源代码中产生一组字节码,它并不是机器代码,但是不管是在Linux还是Windows的机器上,同样的源代码产生的字节码都是一样的,同时它还有个虚拟机,虚拟机会一条一条执行字节码,生成可执行的机器代码交给CPU执行。正是因为字节码和虚拟机这两个特性,使得我们的程序可以正常执行在Linux或Windows机器上
那么你一定好奇,Python的编译器和Python的虚拟机在什么地方呢?于是,我们来到安装Python的目录下,首先我们看到的是python.exe,于是我们怀疑python.exe是不是编译器或者虚拟机其中一个,不过我们发现,python.exe只有92KB,似乎并不大,不太可能支撑起一个庞大的语言。
实际上,Python编译器和Python虚拟机都在同一个文件里,而且还在上面最大的文件里,没错,就是python25.dll这个文件,这个文件既要完成编译工作,同时还要完成解释工作(即虚拟机的工作)
熟悉Java的同学都知道(不熟悉也没关系,举个栗子而已),Java在编译程序的时候,会产生一个class文件,最后调用Java命令执行class文件中的字节码。而Python作为同样的半编译半解释的语言,也有类似的特性,Python执行的时候,有可能会产生一个字节码文件,注意,只是有可能,而Java是一定会产生字节码文件。Python既可以直接执行源代码文件,也可以执行字节码文件,何时会产生这个字节码文件,我们且往后看
PyCodeObject对象与pyc文件
刚刚我们说,Python在执行一个文件时,是有可能产生字节码文件的,那么是在何时才会产生呢?我们可以写一个demo.py,里面随便你写什么程序,然后直接用python执行,会发现,不管我们怎么执行,都不会生成pyc文件。嗯……看来是我们的操作有点问题?那什么才是生成pyc文件的正确操作呢?
当然是当我们执行一个脚本时,脚本引入的模块会产生pyc文件了!
这样说可能还有些人不懂,没关系,我们直接上代码
# ls demo.py # cat demo.py class A: pass def func(): pass a = A() func()
从上面的代码我们可以看到,在当前目录下只有一个demo.py的文件,并且我们打印出这个文件的内容,当然,这个内容不重要,怎么生成pyc文件最重要。我们再来看下面的代码,我们在当前的目录直接进入python命令行,然后引入demo模块再退出
# python ………… >>> from demo import * >>> # ls demo.py demo.pyc
然后我们再打印当前目录下的文件,神奇的事发生了,多了一个demo.pyc文件。
为什么当引入一个Python文件时,这个Python文件对应的pyc文件会生成呢?可以这样认为,当一个文件被引入时,代表这个文件很可能是要经常被引用的,因此Python编译这个文件,生成字节码。而仅仅是执行一个脚本,Python会认为这个脚本只执行一次,后续不会再执行,所以,不会为其生成pyc文件
pyc文件中,存储着一个PyCodeObject对象,对于Python编译器来说,PyCodeObject对象才是真正的编译结果,而pyc文件只是这个对象在磁盘上的表现形式,它们其实是Python对源文件编译的结果的两种不同的存在方式。程序运行期间,编译结果存在于内存中的PyCodeObject对象中,而程序结束后,编译结果又被保存到pyc文件中,当下一次运行相同程序时,Python会根据pyc文件中记录的编译结果,直接在内存中建立PyCodeObject对象,而不用再次编译
PyCodeObject对象
我们先来看Python源码中关于PyCodeObject的声明:
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) */ void *co_zombieframe; /* for optimization only (see frameobject.c) */ } PyCodeObject;
PyCodeObject对象中的各个域所包含的信息我们会在下面一步一步挖掘开来。Python编译器在对Python源代码进行编译的时候,对于代码中的一个Code Block,会创建一个PyCodeObject对象与这段代码对应,那么,在代码中,怎么样才算是一个Code Block呢?Python中是这样定义的:当进入一个新的名字空间,或者说作用域时,我们算是进入一个新的Code Block了。
回顾一下上面的demo.py文件,Python编译器对源代码完成编译后,总共会创建3个PyCodeObject对象,一个是对应demo.py整个文件的,一个是对应class A所代表的Code Block,最后一个是def func所代表的Code Block
这里,我们提及Python中一个至关重要的概念——名字空间,名字空间是符号的上下文环境,符号的含义取决于名字空间。更具体的说,一个变量名对应的变量值是什么,在Python中是不确定的,而是通过名字空间来决定的
对于某个变量名,比如a,在同一个脚本中,可能在某一个类里,它代表的是一串字符串,而在另外一个方法中,它代表的是一个整型值,但a这个变量到底是什么值,就是由名字空间来决定的。名字空间可以一个套一个地形成一条名字空间链,Python虚拟机在执行的过程中,会有很大一部分时间消耗在从这条名字空间链中确定一个符号所对应的对象是什么
正如我们前面所说,一个Code Block就对应一个名字空间,即对应一个PyCodeObject对象。在Python中,类、函数、module都对应着一个独立的名字空间。因此,都会有一个PyCodeObject对象与其对应。所以,demo.py经过Python编译器编译后,一共得到3个PyCodeObject对象
pyc文件
每一个PyCodeObject对象都包含了每一个Code Block中所有Python源代码经过编译后得到的byte code序列,Python会将这些字节码序列和PyCodeObject对象一起存储在pyc文件中。要了解pyc文件,首先我们必须清楚PyCodeObject中大部分域所代表的含义,这一点是无论如何都不能绕过去的
Field | Content |
co_argcount | Code Block的位置参数个数,比如说一个函数的位置参数个数 |
co_nlocals | Code Block中局部变量的个数,包括其位置参数的个数 |
co_stacksize | 执行该段Code Block需要的栈空间 |
co_flags | N/A,表示该域对理解Python虚拟机的行为没太多用处 |
co_code | Code Block编译所得的字节码指令序列,以PyStringObject的形式存在 |
co_consts | PyTuppleObject对象,保存Code Block中所有的常量 |
co_names | PyTuppleObject对象,保存Code Block中所有的符号 |
co_varnames | Code Block中的局部变量名集合 |
co_freevars | Python实现闭包需要用到的东西,为自由变量 |
co_cellvars | Code Block中内部嵌套函数所引用的局部变量名集合 |
co_filename | Code Block所对应的.py文件的完整路径 |
co_name | Code Block的名字,通常是函数名或类名 |
co_firstlineno | Code Block在对应的.py文件中的起始行 |
co_lnotab | 字节码指令与.py文件中的source code行号的对应关系,以PyStringObject的形式存在 |
co_lnotab中的字节码和相应的source code行号的对应信息是以unsigned bytes的数组形式存在的,数组的形式可以看作(字节码指令在co_code中位置,source code行号)形式的一个list,如下面的表格:
表1-1 | |
字节码在co_code中的偏移 | .py文件中源代码的行号 |
0 | 1 |
6 | 2 |
50 | 7 |
这里有个小技巧,Python不会直接记录这些信息,但是它会记录这些信息间的增量值。所以,对应的co_lnotab应该如下表:
表1-2 | |
字节码在co_code中的偏移 | .py文件中源代码的行号 |
0 | 1 |
6(6+0=6) | 1(1+1=2) |
44(44+6+0=50) | 5(5+1+1=7) |
在Python中访问PyCodeObject对象
在Python中,有与C一级的PyCodeObject对象对应的对象——code对象,这个对象是对C一级的PyCodeObject对象的一个简单包装,通过code对象,我们可以访问到PyCodeObject对象中的各个域
>>> source = open("demo.py").read() >>> co = compile(source, "demo.py", "exec") >>> type(co) <class 'code'> >>> dir(co) ['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__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_kwonlyargcount', 'co_lnotab', 'co_name', 'co_names', 'co_nlocals', 'co_stacksize', 'co_varnames'] >>> co.co_names ('A', 'func', 'a') >>> co.co_name '<module>' >>> co.co_filename 'demo.py'