Python源代码剖析笔记3-Python执行原理初探
本文简书地址:http://www.jianshu.com/p/03af86845c95
之前写了几篇源代码剖析笔记,然而慢慢觉得没有从一个宏观的角度理解python执行原理的话,从底向上分析未免太easy让人疑惑。不如先从宏观上对python执行原理有了一个基本了解,再慢慢探究细节。这样或许会好非常多。
这也是近期这么久没有更新了笔记了,一直在看源代码剖析书籍和源代码。希望能够从一个宏观层面理清python执行原理。人说读书从薄读厚,再从厚读薄方是理解了真意。希望能够达到这个境界吧,加了个油。
1 Python执行环境初始化
在看怎么执行之前。先要简单的说明一下python的执行时环境初始化。python中有一个解释器状态对象PyInterpreterState用于模拟进程(后面简称进程对象),另外有一个线程状态对象PyThreadState模拟线程(后面简称线程对象)。python中的PyInterpreterState结构通过一个链表链接起来,用于模拟操作系统多进程。进程对象中有一个指针指向线程集合。线程对象则有一个指针指向其相应的进程对象。这样线程和进程就关联了起来。当然。还少不了一个当前执行线程对象_PyThreadState_Current用来维护当前执行的线程。
1.1 进程线程初始化
python中调用PyInitialize()函数来完毕执行环境初始化。在初始化函数中,会创建进程对象interp以及线程对象并在进程对象和线程对象建立关联。并设置当前执行线程对象为刚创建的线程对象。接下来是类型系统初始化。包含int。str。bool,list等类型初始化,这里留到后面再慢慢分析。
然后。就是另外一个大头,那就是系统模块初始化。进程对象interp中有一个modules变量用于维护全部的模块对象,modules变量为字典对象。当中维护(name, module)相应关系,在python中相应着sys.modules。
1.2 模块初始化
系统模块初始化过程会初始化 __builtin__, sys, __main__, site
等模块。在python中,模块对象是以PyModuleObject结构体存在的,除了通用的对象头部。当中就仅仅有一个字典字段md_dict。
模块对象中的md_dict字段存储的内容是我们非常熟悉的,比方__name__, __doc__
等属性,以及模块中的方法等。
在__builtin__
模块初始化中,md_dict中存储的内容就包含内置函数以及系统类型对象,如len,dir,getattr等函数以及int,str,list等类型对象。正由于如此,我们才干在代码中直接用len函数。由于依据LEGB规则。我们能够在__builtin__
模块中找到len这个符号。差点儿相同的过程创建sys
模块以及__main__
模块。创建完毕后,进程对象interp->builtins
会被设置为__builtin__
模块的md_dict字段,即模块对象中的那个字典字段。
而interp->sysdict
则是被设置为sys模块的md_dict字段。
sys模块初始化后,当中包含前面提到过的modules以及path,version,stdin,stdout,maxint等属性,exit,getrefcount,_getframe等函数。注意这里是设置了主要的sys.path(即python安装文件夹的lib路径等),第三方模块的路径是在site模块初始化的时候增加的。
须要说明的是,__main__
模块是个特殊的模块,在我们写第一个python程序时,当中的__name__ == "__main__"
中的__main__
指的就是这个模块名字。当我们用python xxx.py
执行python程序时,该源文件就能够当作是名为__main__
的模块了,而假设是通过其它模块导入,则其名字就是源文件本身的名字,至于为什么,这个在后面执行一个python程序的样例中会具体说明。
当中另一点要说明的是,在创建__main__
模块的时候。会在模块的字典中插入("__builtins__", __builtin__ module)
相应关系。在后面能够看到这个模块特别重要。由于在执行时栈帧对象PyFrameObject的f_buitins字段就会被设置为__builtin__
模块,而栈帧对象的locals和globals字段初始会被设置为__main__
模块的字典。
另外,site
模块初始化主要用来初始化python第三方模块搜索路径,我们常常常使用的sys.path就是这个模块设置的了。
它不仅将site-packages路径加到sys.path中,还会把site-packages文件夹以下的.pth文件里的全部路径增加到sys.path中。
以下是一些验证代码。能够看到sys.modules中果然有了__builtin__, sys, __main__
等模块。此外,系统的类型对象都已经位于__builtin__
模块字典中。
In [13]: import sys
In [14]: sys.modules['__builtin__'].__dict__['int']
Out[14]: int
In [15]: sys.modules['__builtin__'].__dict__['len']
Out[15]: <function len>
In [16]: sys.modules['__builtin__'].__dict__['__name__']
Out[16]: '__builtin__'
In [17]: sys.modules['__builtin__'].__dict__['__doc__']
Out[17]: "Built-in functions, exceptions, and other objects.\n\nNoteworthy: None is the `nil' object; Ellipsis represents `...' in slices."
In [18]: sys.modules['sys']
Out[18]: <module 'sys' (built-in)>
In [19]: sys.modules['__main__']
Out[19]: <module '__main__' (built-in)>
好了,基本工作已经准备妥当,接下来能够执行python程序了。有两种方式,一种是在命令行以下的交互,第二种是以python xxx.py
的方式执行。在说明这两种方式前。须要先介绍下python程序执行相关的几个结构。
1.3 Python执行相关数据结构
python执行相关数据结构主要由PyCodeObject,PyFrameObject以及PyFunctionObject。
当中PyCodeObject是python字节码的存储结构。编译后的pyc文件就是以PyCodeObject结构序列化后存储的,执行时载入并反序列化为PyCodeObject对象。PyFrameObject是对栈帧的模拟,当进入到一个新的函数时,都会有PyFrameObject对象用于模拟栈帧操作。PyFunctionObject则是函数对象。一个函数相应一个PyCodeObject,在执行def test():
语句的时候会创建PyFunctionObject对象。能够这样觉得,PyCodeObject是一种静态的结构。python源文件确定,那么编译后的PyCodeObject对象也是不变的。而PyFrameObject和PyFunctionObject是动态结构,当中的内容会在执行时动态变化。
PyCodeObject对象
python程序文件在执行前须要编译成PyCodeObject对象。每个CodeBlock都会是一个PyCodeObject对象。在Python中,类,函数。模块都是一个Code Block。也就是说编译后都有一个单独的PyCodeObject对象,因此。一个python文件编译后可能会有多个PyCodeObject对象。比方以下的演示样例程序编译后就会存在2个PyCodeObject对象。一个相应test.py整个文件。一个相应函数test。
关于PyCodeObject对象的解析,能够參见我之前的文章Python pyc格式解析,这里就不赘述了。
#演示样例代码test.py
def test():
print "hello world"
if __name__ == "__main__":
test()
PyFrameObject对象
python程序的字节码指令以及一些静态信息比方常量等都存储在PyCodeObject中,执行时显然不可能仅仅是操作PyCodeObject对象。由于有非常多内容是执行时动态改变的,比方以下这个代码test2.py,尽管1和2处的字节码指令相同。可是它们执行结果显然是不同的,这些信息显然不能在PyCodeObject中存储。这些信息事实上须要通过PyFrameObject也就是栈帧对象来获取。PyFrameObject对象中有locals,globals,builtins三个字段相应local。global。builtin三个名字空间,即我们常说的LGB规则,当然加上闭包,就是LEGB规则。一个模块相应的文件定义一个global作用域,一个函数定义一个local作用域,python自身定义了一个*作用域builtin作用域,这三个作用域分别相应PyFrameObject对象的三个字段,这样就能够找到相应的名字引用。比方test2.py中的1处的i引用的是函数test的局部变量i,相应内容是字符串“hello world”,而2处的i引用的是模块的local作用域的名字i。相应内容是整数123(注意模块的local作用域和global作用域是一样的)。
须要注意的是,函数中局部变量的訪问并不须要去訪问locals名字空间,由于函数的局部变量总是不变的。在编译时就能确定局部变量使用的内存位置。
#演示样例代码test2.py
i = 123
def test():
i = 'hello world'
print i #1
test()
print i #2
PyFunctionObject对象
PyFunctionObject是函数对象,在创建函数的指令MAKE_FUNCTION中构建。PyFunctionObject中有个func_code字段指向该函数相应的PyCodeObject对象。另外还有func_globals指向global名字空间。注意到这里并没有使用local名字空间。调用函数时,会创建新的栈帧对象PyFrameObject来执行函数。函数调用关系通过栈帧对象PyFrameObject中的f_back字段进行关联。终于执行函数调用时。PyFunctionObject对象的影响已经消失。真正起作用的是PyFunctionObject的PyCodeObject对象和global名字空间,由于在创建函数栈帧时会将这两个參数传给PyFrameObject对象。
1.4 Python程序执行过程浅析
说完几个基本对象。如今回到之前的话题,開始准备执行python程序。两种方式交互式和直接python xxx.py
尽管有所不同,但终于归于一处。就是启动虚拟机执行python字节码。
这里以python xxx.py
方式为例,在执行python程序之前,须要对源文件编译成字节码,创建PyCodeObject对象。这个是通过PyAST_Compile函数实现的,至于具体编译流程。这就要參看《编译原理》那本龙书了,这里临时当做黑盒好了,由于单就编译这部分而言,一时半会也说不清楚(好吧,事实上是我也没有学好编译原理)。
编译后得到PyCodeObject对象,然后调用PyEval_EvalCode(co, globals, locals)
函数创建PyFrameObject对象并执行字节码了。注意到參数里面的co是PyCodeObject对象,而由于执行PyEval_EvalCode时创建的栈帧对象是Python创建的第一个PyFrameObject对象。所以f_back为NULL,并且它的globals和locals就是__main__
模块的字典对象。
假设我们不是直接执行。而是导入一个模块的话。则还会将python源代码编译后得到的PyCodeObject对象保存到pyc文件里,下次载入模块时假设这个模块没有修改过就能够直接从pyc文件里读取内容而不须要再次编译了。
执行字节码的过程就是模拟CPU执行指令的过程一样。先指向PyFrameObject的f_code字段相应的PyCodeObject对象的co_code字段,这就是字节码存储的位置,然后取出第一条指令,接着第二条指令…依次执行全然部的指令。
python中指令长度为1个字节或者3个字节。当中无參数的指令长度是1个字节,有參数的指令长度是3个字节(指令1字节+參数2字节)。
python虚拟机的进程,线程,栈帧对象等关系例如以下图所看到的:
2 Python程序执行实例说明
程序员学习一门新的语言往往都是从hello world開始的,一来就跟世界打个招呼,由于接下来就要去面对程序语言未知的世界了。我学习python也是从这里開始的,仅仅是曾经并不去深究它的执行原理,这回是逃只是去了。看看以下的栗子。
#演示样例代码test3.py
i = 1
s = 'hello world'
def test():
k = 5
print k
print s
if __name__ == "__main__":
test()
这个样例代码不多。只是也涉及到python执行原理的方方面面(除了类机制那一块外,类机制那一块还没有理清楚,先不理会)。那么依照之前部分说的,执行python test3.py
的时候,会先初始化python进程和线程,然后初始化系统模块以及类型系统等。然后执行python程序test3.py。
每次执行python程序都是开启一个python虚拟机。由于是直接执行,须要先编译为字节码格式,得到PyCodeObject对象。然后从字节码对象的第一条指令開始执行。由于是直接执行,所以PyCodeObject也就没有序列化到pyc文件保存了。以下能够看下test3.py的PyCodeObject,使用python的dis模块能够看到字节码指令。
In [1]: source = open('test3.py').read()
In [2]: co = compile(source, 'test3.py', 'exec')
In [3]: co.co_consts
Out[3]:
(1,
'hello world',
<code object test at 0x1108eaaf8, file "run.py", line 4>,
'__main__',
None)
In [4]: co.co_names
Out[4]: ('i', 's', 'test', '__name__')
In [5]: dis.dis(co) ##模块本身的字节码,以下说的整数。字符串等都是指python中的对象,相应PyIntObject,PyStringObject等。
1 0 LOAD_CONST 0 (1) # 载入常量表中的第0个常量也就是整数1到栈中。
3 STORE_NAME 0 (i) # 获取变量名i,出栈刚刚载入的整数1,然后存储变量名和整数1到f->f_locals中,这个字段相应着查找名字时的local名字空间。
2 6 LOAD_CONST 1 ('hello world')
9 STORE_NAME 1 (s) #同理。获取变量名s,出栈刚刚载入的字符串hello world,并存储变量名和字符串hello world的相应关系到local名字空间。
4 12 LOAD_CONST 2 (<code object test at 0xb744bd10, file "test3.py", line 4>)
15 MAKE_FUNCTION 0 #出栈刚刚入栈的函数test的PyCodeObject对象。以code object和PyFrameObject的f_globals为參数创建函数对象PyFunctionObject并入栈
18 STORE_NAME 2 (test) #获取变量test,并出栈刚入栈的PyFunctionObject对象,并存储到local名字空间。
9 21 LOAD_NAME 3 (__name__) ##LOAD_NAME会先依次搜索local,global。builtin名字空间,当然我们这里是在local名字空间能找到__name__。
24 LOAD_CONST 3 ('__main__')
27 COMPARE_OP 2 (==) ##比較指令
30 JUMP_IF_FALSE 11 (to 44) ##假设不相等则直接跳转到44相应的指令处,也就是以下的POP_TOP。
由于在COMPARE_OP指令中,会设置栈顶为比較的结果。所以须要出栈这个比較结果。
当然我们这里是相等,所以接着往下执行33处的指令,也是POP_TOP。
33 POP_TOP
10 34 LOAD_NAME 2 (test) ##载入函数对象
37 CALL_FUNCTION 0 ##调用函数
40 POP_TOP ##出栈函数返回值
41 JUMP_FORWARD 1 (to 45) ##前进1步,注意是下一条指令地址+1。也就是44+1=45
>> 44 POP_TOP
>> 45 LOAD_CONST 4 (None)
48 RETURN_VALUE #返回None
In [6]: dis.dis(co.co_consts[2]) ##查看函数test的字节码
5 0 LOAD_CONST 1 (5)
3 STORE_FAST 0 (k) #STORE_FAST与STORE_NAME不同,它是存储到PyFrameObject的f_localsplus中,不是local名字空间。
6 6 LOAD_FAST 0 (k) #相相应的,LOAD_FAST是从f_localsplus取值
9 PRINT_ITEM
10 PRINT_NEWLINE #打印输出
7 11 LOAD_GLOBAL 0 (s) #由于函数没有使用local名字空间。所以,这里不是LOAD_NAME,而是LOAD_GLOBAL,不要被名字迷惑。它实际上会依次搜索global,builtin名字空间。
14 PRINT_ITEM
15 PRINT_NEWLINE
16 LOAD_CONST 0 (None)
19 RETURN_VALUE
依照我们前面的分析。test3.py这个文件编译后事实上相应2个PyCodeObject。一个是本身test3.py这个模块总体的PyCodeObject,另外一个则是函数test相应的PyCodeObject。依据PyCodeObject的结构。我们能够知道test3.py字节码中常量co_consts有5个,各自是整数1,字符串‘hello world’。函数test相应的PyCodeObject对象,字符串__main__
,以及模块返回值None对象。恩,从这里能够发现,事实上模块也是有返回值的。
我们相同能够用dis模块查看函数test的字节码。
关于字节码指令,代码中做了解析。
须要注意到函数中局部变量如k的取值用的是LOAD_FAST。即直接从PyFrameObject的f_localsplus字段取,而不是LOAD_NAME那样依次从local,global以及builtin查找。这是函数的特性决定的。
函数的执行时栈也是位于f_localsplus相应的那片内存中。仅仅是前面一部分用于存储函数參数和局部变量,而后面那部分才是执行时栈使用,这样逻辑上执行时栈和函数參数以及局部变量是分离的。尽管物理上它们是连在一起的。须要注意的是。python中使用了预測指令机制。比方COMPARE_OP常常跟JUMP_IF_FALSE或JUMP_IF_TRUE成对出现。所以假设COMPARE_OP的下一条指令正好是JUNP_IF_FALSE,则能够直接跳转到相应代码处执行,提高一定效率。
此外,还要知道在执行test3.py的时候,模块的test3.py栈帧对象中的f_locals和f_globals的值是一样的。都是__main__
模块的字典。
在test3.py的代码后面加上例如以下代码能够验证这个猜想。
... #test3.py的代码
if __name__ == "__main__":
test()
print locals() == sys.modules['__main__'].__dict__ # True
print globals() == sys.modules['__main__'].__dict__ # True
print globals() == locals() # True
正式由于如此,所以python中函数定义顺序是无关的。不须要跟C语言那样在调用函数前先声明函数。比方以下test4.py是全然正常的代码,函数定义顺序不影响函数调用,由于在执行def语句的时候,会执行MAKE_FUNCTION指令将函数对象增加到local名字空间。而local和global此时相应的是同一个字典。所以也相当于增加了global名字空间,从而在执行函数g的时候是能够找到函数f的。另外也能够注意到,函数声明和实现事实上是分离的,声明的字节码指令在模块的PyCodeObject中执行。而实现的字节码指令则是在函数自己的PyCodeObject中。
#test4.py
def g():
print 'function g'
f()
def f():
print 'function f'
g()
~