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

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

之前写了几篇源码剖析笔记,然而慢慢觉得没有从一个宏观的角度理解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虚拟机的进程,线程,栈帧对象等关系如下图所示:

py.png

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()
~      
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • Python的函数机制是很重要的部分,很多时候用python写脚本,就是几个函数简单解决问题,不需要像java那样...
    __七把刀__阅读 5,641评论 0 12
  • 一、温故而知新 1. 内存不够怎么办 内存简单分配策略的问题地址空间不隔离内存使用效率低程序运行的地址不确定 关于...
    SeanCST阅读 7,784评论 0 27
  • 1. 简单的例子 先从一个简单的例子说起,包含了两个文件 foo.py 和 demo.py 执行这个程序pytho...
    jiangmo阅读 1,690评论 0 5
  • python学习笔记 声明:学习笔记主要是根据廖雪峰官方网站python学习学习的,另外根据自己平时的积累进行修正...
    renyangfar阅读 3,027评论 0 10
  • 要点: 函数式编程:注意不是“函数编程”,多了一个“式” 模块:如何使用模块 面向对象编程:面向对象的概念、属性、...
    victorsungo阅读 1,480评论 0 6