做者:秘塔科技算法研究员 Qian Wan前端
前几天IEEE Spectrum发布了第五届顶级语言交互排行榜,Python语言继续稳坐第一把交椅,而且相比去年的排行状况,拉开了与第二名的距离(去年第二名的排名得分为99.7)。从下图能看出Python的优点仍是很明显的,并且在Web、企业级和嵌入式这三种应用类别的流行度都很高。python
冰冻三尺非一日之寒。Python语言自1990年由Guido van Rossum第一次发布至今已经快三十年的历史,它支持多种操做系统,并以CPython为参考实现。Python语言在不少领域都有杀手级的应用框架,如深度学习方面有PyTorch和Tensorflow,天然语言处理有NLTK,Web框架有Django、Flask,科学计算有Numpy、Scipy,计算机视觉有OpenCV,科学绘图有Matplotlib,爬虫有Scrapy,凡此种种,不一而足。面对这么多不一样种类的Python应用框架,下面一些问题是值得咱们思考的:git
三言两语可能很难比较全面的回答上面一些问题,并且只研究Python语言获得的答案也可能会有失偏颇。可是Python语言的源代码可以为回答这些问题提供一些线索,并且经过阅读源码能让咱们在使用Python语言时看到一些之前咱们看不到的细节,就如同《黑客帝国》电影里的Neo同样能看到母体世界的源代码,也能像Neo那样在机器的世界里飞天遁地。github
咱们使用pyenv花几分钟时间来构建Python运行环境,它不只能够与操做系统原生的Python环境隔离,还能支持多种版本的Python环境,另外也支持在同一Python版本下的多个虚拟环境,能够用来隔离不一样应用的Python依赖包。部署代码以下算法
$ git clone https://github.com/pyenv/pyenv.git ~/.pyenv $ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc $ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc $ git clone https://github.com/pyenv/pyenv-virtualenv.git ${HOME}/.pyenv/plugins/pyenv-virtualenv $ echo 'eval "$(pyenv init -)"' >> ~/.bashrc $ echo 'eval "$(pyenv virtualenv-init -)"' >> ~/.bashrc $ CONFIGURE_OPTS=--enable-shared $HOME/.pyenv/bin/pyenv install 3.6.6 -k -v $ $HOME/.pyenv/bin/pyenv virtualenv 3.6.6 py3.6
部署好了以后每次运行下面命令就能替换掉系统原生的Python环境缓存
$ pyenv activate py3.6
安装后的目录结构以下sass
~/.pyenv/sources/3.6.6/Python-3.6.6
~/.pyenv/versions/3.6.6/include/python3.6m/
~/.pyenv/versions/3.6.6/lib/libpython3.6m.dylib
要深刻剖析Python的源代码,就要对源码中几个大的模块的做用有一个初步的认识。咱们进入到源码目录~/.pyenv/sources/3.6.6/Python-3.6.6
,其中几个跟Python语言直接相关的目录及其功能以下bash
Include
:C头文件,与部署好的头文件目录~/.pyenv/versions/3.6.6/include/python3.6m/
中的文件一致(严格来讲,部署好的头文件目录中会多一个自动生成的pyconfig.h
文件),这些头文件定义了Python语言的底层抽象结构。Lib
:Python语言库,这部分不参与Python的编译,而是用Python语言写好的模块库。Modules
:用C语言实现的Python内置库。Objects
:Python内置对象的C语言实现以及抽象接口的实现。Parser
:Python编译器的前端,词法分析器和语法分析器。后者就是基于龙书的LL(1)实现的。Programs
:可执行文件~/.pyenv/versions/3.6.6/bin/python
的源码所在的目录。Python
:Python虚拟机所在的目录,也是整个Python语言较为核心的部分。使用下面的图示能更好的展现这些目录以前的相互关系,虚线箭头表示提供接口定义,实线箭头表示提供服务,自顶向下的结构也体现了语言设计在架构上的层次关系。数据结构
Include
目录从上面这些模块的大体功能上分析,咱们能够判断出Include
、Objects
和Python
中的代码比较重要。咱们先看一下这三个目录包含的代码量架构
$ cat Include/* Objects/* Python/* | wc -l cat: Objects/clinic: Is a directory cat: Objects/stringlib: Is a directory cat: Python/clinic: Is a directory 215478
21万行代码的阅读量有点略大,咱们仍是先挨个看看这些目录中文件的命名、大小以及一些注释,看能不能获得一些线索。
$ wc -l Include/*.h | sort -k1 ... 324 pystate.h 370 objimpl.h 499 dynamic_annotations.h 503 pyerrors.h 637 Python-ast.h 767 pyport.h 1077 object.h 1377 abstract.h 2342 unicodeobject.h 15980 total
从文件名和文件大小能够初步判断object.h
和abstract.h
是两个比较重要的头文件,实际上它们定义了Python底层的抽象对象以及统一的抽象接口。unicodeobject.h
虽然体积大,可是有不少跟它相似的头文件,如boolobject.h
、longobject.h
、floatobject.h
等等,这些头文件应该是内置类型的头文件,咱们能够暂时不去理会这些文件,对语言的整体理解不会形成困难。
为了避免漏掉一些重要的头文件,咱们快速阅读一下其余头文件中可能包含的一些引导性的注释,发现这些头文件也比较重要:
Python.h
:元头文件,一般在写Python的C扩展时会包含它。ceval.h
:做为Python/ceval.c
的头文件,而Python/ceval.c
负责运行编译后的代码。code.h
:包含字节码相关的底层抽象。compile.h
:抽象语法树的编译接口。objimpl.h
:跟内存相关的抽象对象高层接口,如内存分配,初始化,垃圾回收等等。pystate.h
:线程状态与解释器状态以及它们的接口。pythonrun.h
:Python代码的语法分析与执行接口。经过以上筛选,咱们看看还剩下多少代码:
$ cat object.h abstract.h objimpl.h Python.h ceval.h code.h compile.h pystate.h pythonrun.h | wc -l 3950
核心头文件压缩到不到4千行。
Objects
目录用相似的思路,咱们能从Objects
目录中筛选出一些比较重要的文件
abstract.c
:抽象对象的接口实现。codeobject.c
:字节码对象的实现。object.c
:通用对象操做的实现。obmalloc.c
:内存分配相关实现。typeobject.c
:Type
对象实现。统计一下代码量
$ wc -l abstract.c codeobject.c object.c obmalloc.c typeobject.c 3246 abstract.c 921 codeobject.c 2048 object.c 2376 obmalloc.c 7612 typeobject.c 16203 total
一会儿新增了1.6万行,毕竟是实打实的C语言实现。
另外还有一些具象化的对象实现文件,虽然它们跟longobject.c
和dictobject.c
之类的对象实现相似,都是具体的对象,可是它们跟Python语言特性比较相关,在这里也把它们列出来,作为备份。
classobject.c
:类对象实现。codeobject.c
:代码对象实现。frameobject.c
:Frame对象实现。funcobject.c
:函数对象实现。methodobject.c
:方法对象实现。moduleobject.c
:模块对象实现。顺便统计下行数
$ wc -l classobject.c codeobject.c frameobject.c funcobject.c methodobject.c moduleobject.c 648 classobject.c 921 codeobject.c 1038 frameobject.c 1031 funcobject.c 553 methodobject.c 802 moduleobject.c 4993 total
Objects
目录中合计约2.1万行。经过探索这些源代码,咱们看出Python的一个设计原则就是:一切皆对象。
严格来讲,只有Python语言暴露给外部使用的部分才抽象成了对象,而一些仅在内部使用的数据结构则没有对象封装,如后面会提到的 解释器状态和 线程状态等。
Python
目录依然通过一轮筛选,能获得下面这些比较重要的文件
ast.c
:将具体语法树转换成抽象语法树,主要函数是PyAST_FromNode()
ceval.c
:执行编译后的字节码。ceval_gil.h
:全局解释器锁(Global Interpreter Lock,GIL)的接口。compile.c
:将抽象语法树编译成Python字节码。pylifecycle.c
:Python解释器的顶层代码,包括解释器的初始化以及退出。pystate.c
:线程状态与解释器状态,以及它们的接口实现。pythonrun.c
:Python解释器的顶层代码,包括解释器的初始化以及退出。可以注意到,pylifecycle.c
和pythonrun.c
的功能是相似的,实际上查阅Python开发历史记录能发现前者是由于开发须要从后者分离出来的。统计一下代码的数量:
$ wc -l ast.c ceval.c ceval_gil.h compile.c pystate.c pythonrun.c 5277 ast.c 5600 ceval.c 270 ceval_gil.h 5329 compile.c 958 pystate.c 1596 pythonrun.c 19030 total
这样浓缩下来Include
、Objects
和Python
三个文件夹中比较重要的代码一共大约4.4万行,先不说咱们这样筛选出来的一波有没有漏掉重要信息,其余不少支持性的代码都尚未包含进去。至少目前有了一个大的轮廓,接下来在深刻代码的时候能够慢慢扩展开。
前面讨论了Python源码的主要目录结构,以及其中主要的源文件。这里咱们换一个思路,看看一个Python源文件是如何在Python解释器里面运行的。调用Python的可执行文件~/.pyenv/versions/3.6.6/bin/python
和调用咱们编写的其余C语言程序在方式上并无太大区别,不一样之处在于Python可执行文件读取的Python源文件,并执行其中的代码。Python之于C就如同C之于汇编,只是Python编译的字节码在Python虚拟机上运行,汇编代码直接在物理机上运行(严格来讲还须要转换成机器代码)。
如下面这条Python源文件运行为例来考察Python可执行文件的执行过程(你们能够玩玩这个生命游戏,运气好能看到滑翔机)。
$ python ~/.pyenv/sources/3.6.6/Python-3.6.6/Tools/demo/life.py
既然Python的可执行文件是C语言编译成的,那么必定有C语言的入口函数main
,它就位于Python源码的./Programs/python.c
文件中。
int main(int argc, char **argv) { // ... res = Py_Main(argc, argv_copy); // ... }
顺藤摸瓜,咱们能够梳理出调用树的主干部分。下面的树形结构中,冒号左边为函数名,右边表示函数定义所在的C源文件,树形结构表示函数定义中包含的其余函数嵌套调用。
main: Programs/python.c └─ Py_Main: Modules/main.c ├─ Py_Initialize: Python/pylifecycle.c │ ├─ PyInterpreterState_New: Python/pystate.c │ ├─ PyThreadState_New: Python/pystate.c │ ├─ _PyGILState_Init: Python/pystate.c │ └─ _Py_ReadyTypes: Objects/object.c ├─ run_file: Modules/main.c │ └─ PyRun_FileExFlags: Python/pythonrun.c │ ├─ PyParser_ASTFromFileObject: Python/pythonrun.c │ │ ├─ PyParser_ParseFileObject: Parser/parsetok.c │ │ └─ PyAST_FromNodeObject: Python/ast.c │ └─ run_mod: Python/pythonrun.c │ ├─ PyAST_CompileObject: Python/compile.c │ └─ PyEval_EvalCode: Python/ceval.c │ ├─ PyFrame_New: Objects/frameobject.c │ └─ PyEval_EvalFrameEx: Python/ceval.c └─ Py_FinalizeEx: Python/pylifecycle.c
不得不说,Python源码的可读性很是好,这些函数的命名方式都是自解释的。Python源文件的运行大体分为两个步骤:
Py_Initialize
:初始化过程,主要涉及到解释器状态、线程状态、全局解释器锁以及内置类型的初始化。run_file
:运行源文件,能够分为三个小步骤
PyParser_ASTFromFileObject
:对源文件的文本进行语法分析,获得抽象语法树。PyAST_CompileObject
:将抽象语法树编译成PyCodeObject
对象。PyEval_EvalCode
:在Python虚拟机中运行PyCodeObject
对象。Py_FinalizeEx
:源文件执行结束后的清理工做。用流程图的形式表示上述调用树的主干部分应该更加清晰明了。
须要指出的是,解释器循环真正执行的是PyEval_EvalFrameEx
函数,它的参数是PyFrameObject
对象,该对象为PyCodeObject
对象提供了执行的上下文环境,因此PyFrameObject
和PyCodeObject
都是很是核心的对象。Python提供了一些工具让咱们能够查看编译后的代码对象,即对编译好的函数进行反汇编。下面的例子虽然简单,但已经能给人清晰的直观认识
>>> from dis import dis >>> class C(object): ... def __init__(self, x): ... self.x = x ... def add(self, y): ... return self.x + y ... >>> dis(C) Disassembly of __init__: 3 0 LOAD_FAST 1 (x) 2 LOAD_FAST 0 (self) 4 STORE_ATTR 0 (x) 6 LOAD_CONST 0 (None) 8 RETURN_VALUE Disassembly of add: 5 0 LOAD_FAST 0 (self) 2 LOAD_ATTR 0 (x) 4 LOAD_FAST 1 (y) 6 BINARY_ADD 8 RETURN_VALUE
反编译的结果是一系列的操做码。头文件Include/opcode.h
包含了Python虚拟机的全部操做码。能看出上面simple_tuple
和simple_list
这两个函数反编译后的最大区别么?tuple
是做为常量被加载进来的,而list
的生成还须要调用BUILD_LIST
。缘由在于tuple
在Python的运行时会进行缓存,也就是每次使用无需请求操做系统内核以得到内存空间。对比一下使用tuple
和list
的耗时状况
>>> %timeit x = (1, 2, 3) 10.9 ns ± 0.0617 ns per loop (mean ± std. dev. of 7 runs, 100000000 loops each) >>> %timeit x = [1, 2, 3] 46.5 ns ± 0.186 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
从统计结果能看出,tuple
的在效率上的优点很是明显。若是某一段调用特别频繁的代码中有些list
能够替换成tuple
,千万不要犹豫。
咱们能够试着为文章开头第一个问题提供一些思路。咱们知道,对计算机作任何形式上的抽象都有可能伤害到计算的效率,对于Python来讲有如下几点
PyCodeObject
对象暴露出来这一点就能看出。因此为了提升Python程序的效率,咱们须要深刻了解Python对象的实现原理、PyCodeObject
的特性以及全局解释器和Python虚拟机的限制。之于文章开头的其余问题,咱们将随着Python源码的深刻研究慢慢展开。
如今咱们对Python代码的运行有了一个宏观的理解,并且大量的细节都有待深刻研究。经过对调用树主干部分的梳理,能看出其余比较重要的支持性模块还包括Python抽象对象PyObject
,抽象语法树及其编译,PyCodeObject
对象,PyFrameObject
对象,解释器状态,线程状态,全局解释器锁。在之后的文章中,咱们会分别对这些模块进行探讨。