Python源码剖析笔记3-Python执行原理初探

Python源码剖析笔记3-Python执行原理初探

本文简书地址:http://www.jianshu.com/p/03af86845c95

之前写了几篇源码剖析笔记,然而慢慢觉得没有从一个宏观的角度理解python执行原理的话,从底向上分析未免太容易让人疑惑,不如先从宏观上对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()
~      
时间: 2024-10-21 09:00:40

Python源码剖析笔记3-Python执行原理初探的相关文章

python源码剖析笔记1——Python对象初见

python源码剖析笔记1--Python对象初见 工作整两年了,用python最多,然而对于python内部机制不一定都清楚,每天沉醉于增删改查的简单逻辑编写,实在耗神.很多东西不用就忘记了,比如C语言,正好,python源码用C写的,分析python源码的同时又能温故C语言基础,实在是件很好的事情.另外,还有陈儒大神的<python源码剖析>做指引,分析也不至于没头没脑.期望在一个月的业余时间,能有所小成,以此为记. 1 python中的对象 python中,一切东西都是对象,在c语言实现

Python源码剖析笔记4-内建数据类型

Python源码剖析笔记4-内建数据类型 Python内建数据类型包括整数对象PyIntObject,字符串对象PyStringObject,列表对象PyListObject以及字典对象PyDictObject等.整数对象之前已经分析过了,这一篇文章准备分析下余下几个对象,这次在<python源码剖析>中已经写的很详细的部分就不赘述了,主要是总结一些之前看书时疑惑的地方. 1 整数对象-PyIntObject 参见 python整数对象. 2 字符串对象-PyStringObject 2.1

Python源码剖析笔记2-Python整数对象

Python源码剖析笔记2-Python整数对象 本文简书地址: http://www.jianshu.com/p/0136ed90cd46 千里之行始于足下,从简单的类别开始分析,由浅入深也不至于自己丧失信心.先来看看Python整数对象,也就是python中的PyIntObject对象,对应的类型对象是PyInt_Type. 1 Python整数对象概览 为了性能考虑,python中对小整数有专门的缓存池,这样就不需要每次使用小整数对象时去用malloc分配内存以及free释放内存.pyth

Python源码剖析笔记0 ——C语言基础

python源码剖析笔记0--C语言基础回顾 要分析python源码,C语言的基础不能少,特别是指针和结构体等知识.这篇文章先回顾C语言基础,方便后续代码的阅读. 1 关于ELF文件 linux中的C编译得到的目标文件和可执行文件都是ELF格式的,可执行文件中以segment来划分,目标文件中,我们是以section划分.一个segment包含一个或多个section,通过readelf命令可以看到完整的section和segment信息.看一个栗子: char pear[40]; static

Python源码剖析笔记6-函数机制

Python的函数机制是很重要的部分,很多时候用python写脚本,就是几个函数简单解决问题,不需要像java那样必须弄个class什么的. 本文简书地址:http://www.jianshu.com/p/d00108741a18 1 函数对象PyFunctionObject PyFunctionObject对象的定义如下: typedef struct { PyObject_HEAD PyObject *func_code; /* A code object */ PyObject *func

Python源码剖析笔记5-模块机制

本文简书地址: http://www.jianshu.com/p/14586ec50ab6 python中经常用到模块,比如import xxx,from xxx import yyy这样子,里面的机制也是需要好好探究一下的,这次主要从黑盒角度来探测模块机制,源码分析点到为止,详尽的源码分析见陈儒大神的<python源码剖析>第14章. 1 如何导入模块 首先来看一个导入模块的例子.创建一个文件夹demo5,文件夹中有如下几个文件. [email protected] ~/demo5 $ ls

《python源码剖析》笔记 Python虚拟机框架

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1. Python虚拟机会从编译得到的PyCodeObject对象中依次读入每一条字节码指令, 并在当前的上下文环境中执行这条字节码指令. Python虚拟机实际上是在模拟操作中执行文件的过程 PyCodeObject对象中包含了字节码指令以及程序的所有静态信息,但没有包含 程序运行时的动态信息--执行环境(PyFrameObject) 2.Python源码中的PyFrameObject

《python源码剖析》笔记 python虚拟机中的一般表达式

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.字节码指令 LOAD_CONST:从consts表中读取序号为i的元素并压入到运行时栈中 STORE_NAME:改变local名字空间.从符号表names取序号为i的元素作为变量名, 取运行时栈的栈顶元素作为变量值,完成从变量名到变量值的映射关系的创建. BUILD_MAP:创建一个空的PyDictObject对象,并压入运行时栈 DUP_TOP:将栈顶元素的引用计数增加1,并将它再次

《python源码剖析》笔记 python虚拟机中的函数机制

本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.Python虚拟机在执行函数调用时会动态地创建新的 PyFrameObject对象, 这些PyFrameObject对象之间会形成PyFrameObject对象链,模拟x86平台上运行时栈 2.PyFuctionObject对象 typedef struct { PyObject_HEAD PyObject *func_code: //对应函数编译后的PyCodeObject对象 Py