您好,欢迎访问一九零五行业门户网

Python程序的执行过程包括将源代码转换为字节码(即编译)以及执行字节码

问题:我们每天都要编写一些python程序,或者用来处理一些文本,或者是做一些系统管理工作。程序写好后,只需要敲下python命令,便可将程序启动起来并开始执行:
$ python some-program.py
那么,一个文本形式的.py文件,是如何一步步转换为能够被cpu执行的机器指令的呢?此外,程序执行过程中可能会有.pyc文件生成,这些文件又有什么作用呢?
1. 执行过程虽然从行为上看python更像shell脚本这样的解释性语言,但实际上python程序执行原理本质上跟java或者c#一样,都可以归纳为虚拟机和字节码。python执行程序分为两步:先将程序代码编译成字节码,然后启动虚拟机执行字节码:
虽然python命令也叫做python解释器,但跟其他脚本语言解释器有本质区别。实际上,python解释器包含编译器以及虚拟机两部分。当python解释器启动后,主要执行以下两个步骤:
编译器将.py文件中的python源码编译成字节码虚拟机逐行执行编译器生成的字节码
因此,.py文件中的python语句并没有直接转换成机器指令,而是转换成python字节码。
2. 字节码python程序的编译结果是字节码,里面有很多关于python运行的相关内容。因此,不管是为了更深入理解python虚拟机运行机制,还是为了调优python程序运行效率,字节码都是关键内容。那么,python字节码到底长啥样呢?我们如何才能获得一个python程序的字节码呢——python提供了一个内置函数compile用于即时编译源码。我们只需将待编译源码作为参数调用compile函数,即可获得源码的编译结果。
3. 源码编译下面,我们通过compile函数来编译一个程序:
源码保存在demo.py文件中:
pi = 3.14def circle_area(r): return pi * r ** 2class person(object): def __init__(self, name): self.name = name def say(self): print('i am', self.name)
编译之前需要将源码从文件中读取出来:
>>> text = open('d:\myspace\code\pythoncode\mix\demo.py').read()>>> print(text)pi = 3.14def circle_area(r): return pi * r ** 2class person(object): def __init__(self, name): self.name = name def say(self): print('i am', self.name)
然后调用compile函数来编译源码:
>>> result = compile(text,'d:\myspace\code\pythoncode\mix\demo.py', 'exec')
compile函数必填的参数有3个:
source:待编译源码
filename:源码所在文件名
mode:编译模式,exec表示将源码当作一个模块来编译
三种编译模式:exec:用于编译模块源码
single:用于编译一个单独的python语句(交互式下)
eval:用于编译一个eval表达式
4. pycodeobject通过compile函数,我们获得了最后的源码编译结果result:
>>> result<code object <module> at 0x000001dec2fcf680, file "d:\myspace\code\pythoncode\mix\demo.py", line 1>>>> result.__class__<class 'code'>
最终我们得到了一个code类型的对象,它对应的底层结构体是pycodeobject
pycodeobject源码如下:
/* bytecode object */struct pycodeobject { pyobject_head int co_argcount; /* #arguments, except *args */ int co_posonlyargcount; /* #positional only arguments */ int co_kwonlyargcount; /* #keyword only arguments */ int co_nlocals; /* #local variables */ int co_stacksize; /* #entries needed for evaluation stack */ int co_flags; /* co_..., see below */ int co_firstlineno; /* first source line number */ 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 aren't used in either hash or comparisons, except for co_name, used in both. this is done to preserve the name and line number for tracebacks and debuggers; otherwise, constant de-duplication would collapse identical functions/lambdas defined on different lines. */ py_ssize_t *co_cell2arg; /* maps cell vars which are arguments. */ pyobject *co_filename; /* unicode (where it was loaded from) */ pyobject *co_name; /* unicode (name, for reference) */ pyobject *co_linetable; /* 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 */ /* scratch space for extra data relating to the code object. type is a void* to keep the format private in codeobject.c to force people to go through the proper apis. */ void *co_extra; /* per opcodes just-in-time cache * * to reduce cache size, we use indirect mapping from opcode index to * cache object: * cache = co_opcache[co_opcache_map[next_instr - first_instr] - 1] */ // co_opcache_map is indexed by (next_instr - first_instr). // * 0 means there is no cache for this opcode. // * n > 0 means there is cache in co_opcache[n-1]. unsigned char *co_opcache_map; _pyopcache *co_opcache; int co_opcache_flag; // used to determine when create a cache. unsigned char co_opcache_size; // length of co_opcache.};
代码对象pycodeobject用于存储编译结果,包括字节码以及代码涉及的常量、名字等等。关键字段包括:
字段用途
co_argcount 参数个数
co_kwonlyargcount 关键字参数个数
co_nlocals 局部变量个数
co_stacksize 执行代码所需栈空间
co_flags 标识
co_firstlineno 代码块首行行号
co_code 指令操作码,即字节码
co_consts 常量列表
co_names 名字列表
co_varnames 局部变量名列表
下面打印看一下这些字段对应的数据:
通过co_code字段获得字节码:
>>> result.co_codeb'd\x00z\x00d\x01d\x02\x84\x00z\x01g\x00d\x03d\x04\x84\x00d\x04e\x02\x83\x03z\x03d\x05s\x00'
通过co_names字段获得代码对象涉及的所有名字:
>>> result.co_names('pi', 'circle_area', 'object', 'person')
通过co_consts字段获得代码对象涉及的所有常量:
>>> result.co_consts(3.14, <code object circle_area at 0x0000023d04d3f310, file "d:\myspace\code\pythoncode\mix\demo.py", line 3>, 'circle_area', <code object person at 0x0000023d04d3f5d0, file "d:\myspace\code\pythoncode\mix\demo.py", line 6>, 'person', none)
可以看到,常量列表中还有两个代码对象,其中一个是circle_area函数体,另一个是person类定义体。对应python中作用域的划分方式,可以自然联想到:每个作用域对应一个代码对象。如果这个假设成立,那么person代码对象的常量列表中应该还包括两个代码对象:init函数体和say函数体。下面取出person类代码对象来看一下:
>>> person_code = result.co_consts[3]>>> person_code<code object person at 0x0000023d04d3f5d0, file "d:\myspace\code\pythoncode\mix\demo.py", line 6>>>> person_code.co_consts('person', <code object __init__ at 0x0000023d04d3f470, file "d:\myspace\code\pythoncode\mix\demo.py", line 7>, 'person.__init__', <code object say at 0x0000023d04d3f520, file "d:\myspace\code\pythoncode\mix\demo.py", line 10>, 'person.say', none)
因此,我们得出结论:python源码编译后,每个作用域都对应着一个代码对象,子作用域代码对象位于父作用域代码对象的常量列表里,层级一一对应。
至此,我们对python源码的编译结果&mdash;&mdash;代码对象pycodeobject有了最基本的认识,后续会在虚拟机、函数机制、类机制中进一步学习。
5. 反编译字节码是一串不可读的字节序列,跟二进制机器码一样。如果想读懂机器码,可以将其反汇编,那么字节码可以反编译吗?
通过dis模块可以将字节码反编译:
>>> import dis>>> dis.dis(result.co_code) 0 load_const 0 (0) 2 store_name 0 (0) 4 load_const 1 (1) 6 load_const 2 (2) 8 make_function 010 store_name 1 (1)12 load_build_class14 load_const 3 (3)16 load_const 4 (4)18 make_function 020 load_const 4 (4)22 load_name 2 (2)24 call_function 326 store_name 3 (3)28 load_const 5 (5)30 return_value
字节码反编译后的结果和汇编语言很类似。其中,第一列是字节码的偏移量,第二列是指令,第三列是操作数。以第一条字节码为例,load_const指令将常量加载进栈,常量下标由操作数给出,而下标为0的常量是:
>>> result.co_consts[0]3.14
这样,第一条字节码的意义就明确了:将常量3.14加载到栈。
由于代码对象保存了字节码、常量、名字等上下文信息,因此直接对代码对象进行反编译可以得到更清晰的结果:
>>>dis.dis(result) 1 0 load_const 0 (3.14) 2 store_name 0 (pi) 3 4 load_const 1 (<code object circle_area at 0x0000023d04d3f310, file "d:\myspace\code\pythoncode\mix\demo.py", line 3>) 6 load_const 2 ('circle_area') 8 make_function 0 10 store_name 1 (circle_area) 6 12 load_build_class 14 load_const 3 (<code object person at 0x0000023d04d3f5d0, file "d:\myspace\code\pythoncode\mix\demo.py", line 6>) 16 load_const 4 ('person') 18 make_function 0 20 load_const 4 ('person') 22 load_name 2 (object) 24 call_function 3 26 store_name 3 (person) 28 load_const 5 (none) 30 return_valuedisassembly of <code object circle_area at 0x0000023d04d3f310, file "d:\myspace\code\pythoncode\mix\demo.py", line 3>: 4 0 load_global 0 (pi) 2 load_fast 0 (r) 4 load_const 1 (2) 6 binary_power 8 binary_multiply 10 return_valuedisassembly of <code object person at 0x0000023d04d3f5d0, file "d:\myspace\code\pythoncode\mix\demo.py", line 6>: 6 0 load_name 0 (__name__) 2 store_name 1 (__module__) 4 load_const 0 ('person') 6 store_name 2 (__qualname__) 7 8 load_const 1 (<code object __init__ at 0x0000023d04d3f470, file "d:\myspace\code\pythoncode\mix\demo.py", line 7>) 10 load_const 2 ('person.__init__') 12 make_function 0 14 store_name 3 (__init__) 10 16 load_const 3 (<code object say at 0x0000023d04d3f520, file "d:\myspace\code\pythoncode\mix\demo.py", line 10>) 18 load_const 4 ('person.say') 20 make_function 0 22 store_name 4 (say) 24 load_const 5 (none) 26 return_valuedisassembly of <code object __init__ at 0x0000023d04d3f470, file "d:\myspace\code\pythoncode\mix\demo.py", line 7>: 8 0 load_fast 1 (name) 2 load_fast 0 (self) 4 store_attr 0 (name) 6 load_const 0 (none) 8 return_valuedisassembly of <code object say at 0x0000023d04d3f520, file "d:\myspace\code\pythoncode\mix\demo.py", line 10>: 11 0 load_global 0 (print) 2 load_const 1 ('i am') 4 load_fast 0 (self) 6 load_attr 1 (name) 8 call_function 2 10 pop_top 12 load_const 0 (none) 14 return_value
操作数指定的常量或名字的实际值在旁边的括号内列出,此外,字节码以语句为单位进行了分组,中间以空行隔开,语句的行号在字节码前面给出。例如pi = 3.14这个语句就被会变成了两条字节码:
1 0 load_const 0 (3.14) 2 store_name 0 (pi)
6. pyc如果将demo作为模块导入,python将在demo.py文件所在目录下生成.pyc文件:
>>> import demo
pyc文件会保存经过序列化处理的代码对象pycodeobject。这样一来,python后续导入demo模块时,直接读取pyc文件并反序列化即可得到代码对象,避免了重复编译导致的开销。只有demo.py有新修改(时间戳比.pyc文件新),python才会重新编译。
因此,对比java而言:python中的.py文件可以类比java中的.java文件,都是源码文件;而.pyc文件可以类比.class文件,都是编译结果。只不过java程序需要先用编译器javac命令来编译,再用虚拟机java命令来执行;而python解释器把这两个过程都完成了。
以上就是python程序的执行过程包括将源代码转换为字节码(即编译)以及执行字节码的详细内容。
其它类似信息

推荐信息