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()
~

Python源代码剖析笔记3-Python运行原理初探的更多相关文章

  1. SpringBoot-02 运行原理初探

    SpringBoot-02 运行原理初探 本篇文章根据b站狂神编写 pom.xml 2.1.父依赖 其中它主要是依赖一个父项目,主要是管理项目的资源过滤及插件! <parent> < ...

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

    本文为senlie原创,转载请保留此地址:http://blog.csdn.net/zhengsenlie 1. Python虚拟机会从编译得到的PyCodeObject对象中依次读入每一条字节码指令 ...

  3. 《python源代码剖析》笔记 Python的编译结果

    本文为senlie原创.转载请保留此地址:http://blog.csdn.net/zhengsenlie 1.python的运行过程 1)对python源码进行编译.产生字节码 2)将编译结果交给p ...

  4. python中函数和生成器的运行原理

    #!/usr/bin/env python # -*- coding:utf-8 -*- # author:love_cat # python的函数是如何工作的 # 比方说我们定义了两个函数 def ...

  5. [python学习手册-笔记]001.python前言

    001.python前言 ❝ 本系列文章是我个人学习<python学习手册(第五版)>的学习笔记,其中大部分内容为该书的总结和个人理解,小部分内容为相关知识点的扩展. 非商业用途转载请注明 ...

  6. [python学习手册-笔记]002.python核心数据类型

    python核心数据类型 ❝ 本系列文章是我个人学习<python学习手册(第五版)>的学习笔记,其中大部分内容为该书的总结和个人理解,小部分内容为相关知识点的扩展. 非商业用途转载请注明 ...

  7. Python基础学习笔记(一)python发展史与优缺点,岗位与薪资

    相信有好多朋友们都是第一次了解python吧,可能大家也听过或接触过这个编程语言.那么到底什么是python呢?它在什么机缘巧合下诞生的呢?又为什么在短短十几年时间内就流行开来呢?就请大家带着疑问,让 ...

  8. Python+Selenium学习笔记5 - python官网的tutorial - 交互模式下的操作

    这篇笔记主要是从Python官网的Tutorial上截取下来,再加上个人理解 1. 在交互模式下,下划线'_'还可以表示上一步的计算结果 2.引号转义问题. 从下图总结的规律是,字符串里的引号如果和引 ...

  9. 狂神说SpringBoot02:运行原理初探

    狂神说SpringBoot系列连载课程,通俗易懂,基于SpringBoot2.2.5版本,欢迎各位狂粉转发关注学习. 微信公众号:狂神说(首发)    Bilibili:狂神说Java(视频) 未经作 ...

随机推荐

  1. 微信公众平台快速开发框架 For Core 2.0 beta –JCSoft.WX.Core 5.2.0 beta发布

    写在前面 最近比较忙,都没有好好维护博客,今天拿个半成品来交代吧. 记不清上次关于微信公众号快速开发框架(简称JCWX)的更新是什么时候了,自从更新到支持.Net Framework 4.0以后基本上 ...

  2. 三星R428 内存不兼容金士顿2G DDR3

    京东上买了个金士顿2G DDR3, 回家装上之后发现不兼容, 原机带的是三星DDR3 1066的2G条子,买的是 金士顿DDR3 2G 1333的条子,结果单独插任何一根都好使,两个插槽均无问题,但是 ...

  3. 【OpenCV】通过ROI区域以及掩码实现图像叠加

    在图像处理领域,我们常常需要设置感兴趣区域(ROI,region of interest),来专注或者简化我们的工作过程 .也就是从图像中选择的一个图像区域,这个区域是我们图像分析所关注的重点.我们圈 ...

  4. 用python模拟登录(解析cookie + 解析html + 表单提交 + 验证码识别 + excel读写 + 发送邮件)

    老婆大人每个月都要上一个网站上去查数据,然后做报表. 为了减轻老婆大人的工作压力,所以我决定做个小程序,减轻我老婆的工作量. 准备工作 1.tesseract-ocr 这个工具用来识别验证码,非常好用 ...

  5. C# 扩展方法使用

    为指定类型扩展方法: 定义类Class1: public static class Class1                                  //必须为static类,且不能包含 ...

  6. Android Task 任务

    关于Android中的组件和应用,之前涉及,大都是静态的概念.而当一个应用运行起来,就难免会需要关心进程.线程这样的概念.在Android中,组件的动态运行,有一个最与众不同的概念,就是Task,翻译 ...

  7. Python3 下实现 Tencent AI 调用

    1.背景 a.鹅厂近期发布了自己的AI api,包括身份证ocr.名片ocr.文本分析等一堆API,因为前期项目用到图形OCR,遂实现试用了一下,发现准确率还不错,放出来给大家共享一下. b.基于py ...

  8. 延迟执行之 Invoke 函数

    Invoke 函数需要继承 MonoBehaviour 类后才能使用. Invoke(string str,float a):a 秒后执行名为 str 函数(只会调用一次). Invoke(strin ...

  9. 为什么大家觉得自学HTML5难?

    互联网发展到今天,越来越多的技术岗位人才出现了稀缺的状态,就拿当前的HTML5来讲,基本成为了每家互联网公司不可缺少的人才.如果抓住这个机会,把HTML5搞好,那么前途不可限量,而且这门行业是越老越吃 ...

  10. windows下安装和redis主从配置(通过哨兵控制主从切换)

    首先自己先得了解什么是redis,这里就不详做介绍什么是redis了,这篇文章主要讲的是怎么样配置 redis怎样配置主从关系和哨兵控制主从服务器的配置以及应用,就当是给自己记笔记吧! 1.下载red ...