cpython 教你阅读 Cpython 的源码(一)
正文:12920字,7位数
预计阅读时间:33分钟
来源:https://realpython.com/cpython-source-code-guide,
译者:陈翔安
就像题目一样,这篇文章是教你关于Cpython的文章。因为内容太长,打算先分开写,以后再合并。
前言
这篇文章很长,但很有用。如果你决定学习Cpython,我希望你能阅读它,你会发现它是一个很好的学习材料。
本文共分为五个部分。你可以根据自己的情况合理安排阅读时间。每个部分都需要一定的时间。通过学习一些案例,你会有成就感,因为你掌握了Python的核心概念,这让你成为了一个更好的Python程序员。
第一部分介绍了Cpython
其实我们平时说的python大多指的是Cpython,它是众多Python中的一种,除了Pypy,Jpython等等。CPython也是官方Python版本,还有很多在线案例。所以这里主要说一下Cpython。
注意:本文是为3.8.0b3版CPython源代码写的。
源代码里有什么?
CPython源代码分发包括各种工具、库和组件。我们将在本文中讨论这些内容。
首先,我们将关注编译器。先从git下载Cpython源代码。
git clonehttps://github . com/python/cpython
cdcpython
git check out 3 . 8 . 0 B3 #切换我们需要的分支
注意:如果没有Git,可以直接从GitHub网站下载ZIP文件形式的源代码。
解压我们下载的文件,它的目录结构如下:
cpython/
│
├──文档←源代码文档描述
├──语法←计算机可读语言定义
├──包含← C语言头文件(头文件中通常有一些可重用的代码)
├──lib←python编写的标准库文件
├── Mac ← Mac支持文件
├──杂项←杂项
├ ──模块编写的标准库文件← C
├──对象←核心类型和对象模块
├──解析器← Python解析器源代码
├──PC←windows编译支持的文件
├── PCbuild ←旧版本Windows系统支持的编译文件
├ ─ ─程序python可执行文件和其他二进制文件的源代码
├── Python ← CPython解析器源代码
└──工具←用于构建或扩展Python的独立工具
接下来,我们将从源代码中编译CPython。
这一步需要一个C编译器和一些构建工具。不同的系统编译不同。这里我用的是mac系统。
在macOS上编译CPython很简单。在终端中,运行以下命令安装C编译器和工具包:
$ xcode-选择-安装
这个命令会弹出一个提示下载安装一套工具,包括Git,Make,GNU C编译器。
你还需要一个OpenSSL的工作副本来从PyPi.org网站获取包。
如果您计划将来使用此版本安装其他软件包,则需要SSL身份验证。
在macOS上安装OpenSSL最简单的方法就是使用HomeBrew。
如果您已经安装了自制程序,您可以使用brew安装命令来安装CPython的依赖项。
$ brew install openssl xz zlib
现在您已经有了依赖关系,您可以在Cpython目录中运行配置脚本:
$ CPPFLAGS= "-I$(brew -前缀zlib)/include "
LDFLAGS= "-L$(brew -前缀zlib)/lib "
。/configure-with-OpenSSL = $(brew-prefix OpenSSL)-with-pydebug
在上面的安装命令中,
CPPFLAGS是c和c++编译器的一个选项,它指定zlib头文件的位置。
LDFLAGS是编译器(如gcc)使用的一些优化参数,其中指定了zlib库文件的位置。
(brew - prefix openssl)这部分意思是在终端执行括号中的命令,显示openssl的安装路径。您可以提前执行括号中的命令,并用返回的结果替换它们。效果是一样的。每行末尾的反斜杠可以使三行作为一个命令执行,而不是在换行时执行命令。
运行上述命令后,将在存储库的根目录下生成一个Makefile,您可以使用它来自动化构建过程。那个。/configure步骤只需运行一次。
您可以通过运行以下命令来构建CPython二进制文件。
$ make -j2 -s
-j2标志允许make同时运行两个作业。如果你有4核,可以改成4。-s标志将阻止Makefile打印它运行到控制台的每个命令。可以删除。输出项目太多。在构建过程中,您可能会收到一些错误。在总结中,它将通知您并非所有的包都可以构建。
例如,_dbm、_sqlite3、_uuid、nis、ossaudiodev、spwd和_tkinter将不使用这组指令来构建。如果您不打算开发这些包,这个错误没有影响。如果你真的需要,你可以参考https://devguide.python.org/.
构建和生成一个名为python.exe的二进制文件需要几分钟的时间。每次更改源代码时,都需要重新运行make来编译。
Python.exe二进制文件是CPython的调试二进制文件。执行以下命令查看Python的运行版本。
$ ./python.exe
Python 3.8.0b3 (tags/v3。8.0b3:4336222407,2019年8月21日,10:00:03)
【铿锵10.0。1(铿锵- 1001.0。46.4)]在达尔文
键入“帮助”、“版权”、“学分”或“许可证”以获取更多信息。
>。>。>。
(其实最新的一个已经到了Python3.9,我整理效果如下。)
编译器做了什么?
编译器的目的是将一种语言转换成另一种语言。我们可以把编译过程比作翻译,把英文的“hello”翻译成中文的“Hello”。
有些编译器把代码编译成只有机器才能理解的,可以在系统上直接执行的机器码。其他编译器会编译成中间语言,由虚拟机执行。
选择编译器的一个重要决定是系统可移植性的要求。Java和。NET CLR会编译成中间语言,这样编译出来的代码就可以适应其他的系统类型。C,Go,C ++和Pascal会编译成低级可执行文件,只能在类似编译的系统上运行。
一般我们直接发布Python的源代码,然后通过Python命令直接运行。事实上,在内部,CPPython会在运行时编译您的代码。大多数人认为Python是一种解释性语言。
严格来说,其实是编译型的。
Python代码不编译成机器码。
它被编译成一种特殊的低级中间语言,只有CPython才能理解。在Python3中,字节码存储在。隐藏目录中的pyc文件,它为下次快速执行提供了一个缓存。因此,如果你在不改变源代码的情况下运行同一个Python应用程序两次,第二次总是会快得多。原因是第二次直接加载字节码,然后运行程序,不像第一次需要编译。
为什么CPython是用c写的而不是Python?
CPPython中的C是对C编程语言的引用,暗示这个Python发行版是用C语言写的。
CPython中的编译器都是用纯C写的,然而很多标准库模块都是用纯Python或者C和Python的组合写的。
那么为什么CPython是用c写的而不是Python呢?
答案在于编译器是如何工作的。
有两种类型的编译器:
自托管编译器是用它们编译的语言编写的编译器,例如 Go 编译器。源到源编译器是用另一种已经有编译器的语言编写的编译器。 这也就意味着如果从头开始编写新的编程语言,则需要一个可执行的应用程序来编译你的编译器!你就需要一个编译器来执行任何操作,因此在开发新语言时,它们通常首先用较旧的,更成熟的语言编写。同时节省时间和学习成本。 一个很好的例子就是 Go 语言。 第一个 Go 编译器是用 C 编写的,然后 Go 可以编译,编译器就在 Go 中重写了。CPython保留了它的C特性:很多标准库模块(比如ssl模块或者sockets模块)都是用C语言编写的,用于访问底层操作系统API。
Windows和Linux内核中用于创建网络套接字、使用文件系统或与监视器交互的API都是用c语言编写的。所以将Python的可扩展性层集中在C语言上是有意义的。在本文的后面,我们将介绍Python标准库和C模块。另外还有一个用Python写的Python编译器,叫PyPy。
PyPy的logo是一个Ouroboros,代表编译器的自我管理特性。Python交叉编译器的另一个例子是Jython。
还有一个是Jython。Jython用Java编写,从Python源代码编译成Java字节码。就像CPython可以轻松导入C库并从Python中使用它们一样,Jython使得导入和引用Java模块和类变得很容易。
Python语言规范
CPython源代码包含Python语言的定义。这是所有Python解释器使用的参考规范。该规范是人类可读和机器可读的格式。文档中详细描述了Python语言、允许的内容和每个语句的行为。
文件
在文档/引用目录中,重构文本文件用Python语言解释了每个函数属性。这是docs.python.org的官方Python参考指南。
目录中有您需要了解整个语言、结构和关键字的文件:
cpython/Doc/参考
|
├──化合物
├──数据模型
├──行政模式第一
├──表情
├──语法
├──进口公司
├──指数
├──介绍
├──词汇分析
├──simple _ stmt . rst
└──顶级组件
在compound _ stmts.rst文件中,您可以看到一个定义with语句的简单示例。with语句可以在Python中以多种方式使用,其中最简单的是上下文管理器和嵌套代码块的实例化:
withx:
...
您可以使用将其重命名为
withx asy:
...
您也可以同时定义多个链
withx asy,z asjk:
...
接下来,我们将探索Python语言的计算机可读文档。
语法
本文档包含存储在单个语法/语法文件中的人类可读规范和机器可读规范。
语法文件是用一种叫做巴克斯-诺尔形式(BNF)的上下文符号编写的。
BNF不是Python特有的,在许多其他语言中经常被用作语法符号。
编程语言中的语法结构受到了诺姆·乔姆斯基在20世纪50年代关于句法结构的工作的启发。
Python的语法文件使用带正则表达式语法的扩展BNF(EBNF)规范。
因此,在语法文件中,您可以使用:
*重复+至少重复一次[]为可选部分|任选一个用于分组 如果在语法文件中搜索 with 语句,你将看到 with 语句的定义: .. productionlist::with_stmt:"with"`with_item `("," ` with _ item `) *:` suite ` s
with _ item:` expression `[" as " ` target `]
引号中的一切都是字符串,这是一个关键字的定义。因此,with_stmt被指定为:
1 .以单词开头
2.接下来是with_item,它是一个测试和(可选)作为表达式。
3.多个项目用逗号分隔
4.以字符结尾:
5.其次是套房。
这两行中提到了其他一些定义:
suite是指具有一个或多个语句的代码块。test是指一个被评估的简单语句。expr指的是一个简单的表达式 如果你想详细探索这些内容,可以在此文件中定义整个 Python 语法。如果想看最近如何使用语法的例子,比如在PEP572中,语法文件中增加了:=运算符。
ATEQUAL'@= '
' RARROW '-& gt;'
省略号“...”
+COLONIAL ':= '
过强;管理员
ERRORTOKEN
使用pgen
Python编译器不使用语法文件本身。
是使用名为pgen的工具创建的解析器表。Pgen读取语法文件并将其转换成解析器表。如果对语法文件进行更改,必须重新生成解析器表并重新编译Python。
注意:pgen应用在Python3 .8中从C语言重写为纯Python语言。
为了了解pgen是如何工作的,让我们改变Python语法的一部分。并重新编译运行Python。
我在语法路径下看到了两个文件,语法和令牌。我们在语法中搜索pass_stmt,然后看到如下
pass_stmt: 'pass '
我们修改一下,改成下面这样
pass_stmt:“传递“|”继续”
在Cpython的根目录下使用make regen-Grammar命令运行pgen重新编译语法文件。
您应该会看到类似这样的输出,表明已经生成了新的Include/graminit.h和Python/graminit.c文件:
以下是部分输出
#重新生成Include/graminit.h和Python/graminit.c
#来自语法/使用pgen的语法
PYTHONPATH=。python3 -m Parser.pgen。/语法/语法
。/语法/标记
。/ Include/graminit.h. new
。/Python/graminit.c. new
蟒蛇3。/Tools/s/update_file.py。/ Include/graminit.h。/ Include/graminit.h. new
蟒蛇3。/Tools/s/update_file.py。/Python/graminit.c。/Python/graminit.c. new
使用重新生成的解析器表,需要重新编译CPython来查看新的语法。使用以前用于操作系统的相同编译步骤。
make-j4 -s
如果代码编译成功,执行新的CPython二进制文件并启动REPL。
。/python.exe
在REPL,您现在可以尝试定义一个函数,用编译成Python语法的proceed关键字替换pass语句。
Python 3.8.0b3 (tags/v3。8.0b3:4336222407,2019年8月21日,10:00:03)
【铿锵10.0。1(铿锵- 1001.0。46.4)]在达尔文
键入“帮助”、“版权”、“学分”或“许可证”以获取更多信息。
>。>。>。失败示例:
...进行
...
>。>。>。例子
以下是我手术的结果。有趣的是没有出错。
接下来,我们将讨论令牌文件及其与语法的关系。记号
与语法文件夹中的语法文件一起,它是一个标记文件,包含解析树中作为叶节点的每个唯一类型。稍后我们将深入介绍解析器树。每个令牌也有一个名称和一个生成的唯一标识,用于简化令牌化器中的引用。
注意:令牌文件是Python3 .8中的一个新函数。
比如左括号叫LPAR,分号叫SEMI。
您将在本文后面看到这些标签:
LPAR '('
RPAR ')'
LSQB '['
' RSQB ']'
冒号“:”
逗号','
SEMI ';'
与语法文件一样,如果您更改了令牌文件,您需要再次运行pgen。
要查看操作中的令牌,可以使用CPython中的tokenize模块。创建一个名为test _ tokens.py的简单Python脚本:
#你好世界!
defmy_function:
进行
然后通过标准库中内置的名为tokenize的模块传递该文件。您将按行和字符查看令牌列表。使用-e标志输出确切的令牌名称:
0,0-0,0: ENCODING 'utf-8 '
1,0-1,14: COMMENT'# Hello world!'
1,14-1,15: NL 'n '
2,0-2,3: NAME'def '
2,4-2,15: NAME'my_function '
2,15-2,16: LPAR '('
2,16-2,17: RPAR ')'
2,17-2,18: COLON ':'
2,18-2,19: NEWLINE'n '
3,0-3,3: INDENT ' '
3,3-3,7:名称“继续”
3,7-3,8: NEWLINE'n '
4,0-4,0: DEDENT ' '
4,0-4,0: ENDMARKER ' '
在输出中,第一列是行/列坐标的范围,第二列是令牌的名称,最后一列是令牌的值。
在输出中,tokenize模块暗示了文件中没有的一些标记。
utf-8的ENCODING标记末尾有一行空,它关闭了函数声明,并用ENDMARKER结束文件。Tokenize模块用纯Python编写,在CPPython源代码中位于Lib/tokenize.py。重要说明:CPython源代码中有两个记号化器:一个是用Python写的,上面演示的那个,一个是用c语言写的。用Python写的是作为一个实用程序,用C写的是用Python编译器。然而,它们具有相同的输出和行为。用C语言写的版本是为了性能而设计的,Python中的模块是为了调试而设计的。
要查看c语言的tokenizer的详细信息,可以用-d标志运行Python。
使用前面创建的test_tokens.py脚本,并使用以下命令运行它:
。/python . exe-d test _ token . py
获得了以下结果
令牌名称/ 'def '...这是一个关键词
DFA 'file_input ',状态0:推送' stmt '
DFA 'stmt ',状态0:推送' compound_stmt '
DFA 'compound_stmt ',状态0: Push 'funcdef '
DFA“funcdef”,状态0: Shift。
令牌名称/“我的函数”...这是我们知道的象征
DFA“funcdef”,状态1: Shift。
令牌LPAR/“(”...这是我们知道的象征
DFA“funcdef”,状态2:推送“参数”
DFA“参数”,状态0:移位。
令牌RPAR/')'...这是我们知道的象征
DFA“参数”,状态1: Shift。
DFA“参数”,状态2:直接弹出。
Token COLON/“:”...这是我们知道的象征
DFA“funcdef”,状态3: Shift。
Token NEWLINE/' '...这是我们知道的象征
DFA 'funcdef ',状态5:[将func_body_suite切换到suite]推送' suite '
DFA 'suite ',状态0: Shift。
Token INDENT/' '...这是我们知道的象征
DFA 'suite ',状态1: Shift。
令牌名称/“继续”...这是一个关键词
DFA '套件',状态3:推送' stmt '
...
接受。
在输出中,您可以看到它被突出显示为关键字。在下一章中,我们将看到如何将Python二进制文件执行到标记器,以及从那里执行代码时会发生什么。现在您已经概述了Python语法以及标记和语句之间的关系,有一种方法可以将pgen输出转换为交互式图形。
下面是Python 3.8a2语法的截图:
看不清楚也没关系。用于生成该图的Python包(instaviz)将在下一章介绍。先了解一下。
Python中的内存管理
在本文中,您将看到对PyArena对象的引用。
Arena是CPython的内存管理结构之一。Python/pyarena.c中的代码包括c的内存分配和解除分配方法。
在编写的C程序中,开发人员应该在写入数据之前为数据结构分配内存。这种分配将内存标记为属于操作系统的进程。开发人员还可以在已分配的内存不再使用时将其解除分配或“释放”,并返回到操作系统的可用内存块表中。如果一个进程为一个变量分配内存,比如在一个函数或循环中,当函数完成时,内存不会自动返回给C语言的操作系统。因此,如果没有在C代码中明确发布,就会造成内存泄漏。每次函数运行,进程都会继续占用更多内存,直到最终系统内存耗尽崩溃!Python把这个责任从程序员身上拿走,使用了两种算法:引用计数器和垃圾收集器。每当解释器被实例化时,PyArena方法在解释器中创建并附加一个内存区域。在CPython解释器的生命周期中,可以分配竞技场。它们与链表相关联。
Arenas将指向Python对象的指针列表存储为PyListObject方法。每次创建新的Python对象时,都会使用PyArena_AddPyObject方法向其添加一个指针。
函数调用将指针存储在arenas列表a_objects中。PyArena方法提供了第二个功能,就是分配和引用原来的内存块列表。例如,如果添加数千个附加值,C代码中的PyList将需要额外的内存。但是PyList不直接分配内存。这个对象通过从PyArena中调用PyArena_Malloc来获取PyArena的原始内存块。在对象分配模块中,您可以为Python对象分配、释放和重新分配内存。分配块的链表存储在arenas中,所以当解释器停止时,可以使用PyArena_Free一次性释放所有托管内存块。
以PyListObject为例。如果你用。追加将一个对象放在Python列表的末尾,不需要重新分配内存,而是使用现有列表中的内存。
那个。append方法调用list_resize来处理列表的内存分配。每个list对象都保存一个已分配内存量的列表。如果要添加的项目适合现有的可用内存,只需添加它。如果列表需要更多内存空,它将被扩展。列表长度扩展到0,4,8,16,25,35,46,58,72,88。
您可以通过调用PyMem_Realloc来扩展列表中分配的内存。
PyMem_Realloc是pymalloc_realloc的API包装器。Python还有一个特殊的c调用malloc的包装器,设置内存分配的最大大小,帮助防止缓冲区溢出错误(参见PyMem_RawMalloc)。
总而言之:
原始内存块的分配是通过PyMem_RawAlloc完成的。Python 对象的指针存储在PyArena中。PyArena还存储了已分配内存块的链表。 有关 API 的更多信息,请参阅 CPython 文档。引用计数要在Python中创建变量并赋值,变量名必须是1。
my_variable= 180392
每当在Python中给变量赋值时,都会在局部变量和全局变量的作用域中检查该变量的名称,以查看它是否已经存在。因为my_variable不在局部变量或全局变量字典中,所以创建了这个新对象,并将该值指定为数字常量180392。现在有了对my_variable的引用,所以my_variable的引用计数器加1。
你可以在CPython的c源代码里看到函数Py _ INCREF和Py _ DECREF。
这两个函数分别计算对象的增量和减量。当变量超出声明的范围时,对对象的引用将减少。Python中的Scope可以指函数或方法、生成器或lambda函数。这些是一些更直观的作用域,但是还有很多其他的隐式作用域,比如将变量传递给函数调用。递增和递减引用的处理是在CPython编译器和核心执行循环ceval.c文件中进行的。我们将在本文后面详细介绍它。
每当调用Py _ DECREF并且计数器变为0时,就调用PyObject_Free函数。对于这个对象,所有分配的内存都调用PyArena_Free。
垃圾收集
CPython的垃圾收集器默认是启用的,它发生在后台,用于释放不再使用的对象的内存。
因为垃圾收集算法比引用计数器复杂得多,不会一直发生,否则会消耗大量CPU资源。经过一定次数的操作,会有规律的发生。CPython的标准库自带一个Python模块,用于连接arena和垃圾收集器的gc模块。
以下是如何在调试模式下使用gc模块:
>。>。>。导入gc
>。>。>。gc.set_debug(gc。调试_统计)
这将在您运行垃圾收集器时打印统计信息。
您可以通过调用get_threshold来获取运行垃圾收集器的阈值:
>。>。>。gc.get_threshold
( 700, 10, 10)
您还可以获得当前阈值计数:
>。>。>。gc.get_count
( 688, 1, 1)
最后,您可以手动运行收集算法:
>。>。>。gc.collect
24
这个调用收集在Modules/gcmodule.c文件中,该文件包含垃圾收集器算法的实现。
结论
在第1部分中,我们介绍了源代码库的结构,如何从源代码和Python语言规范进行编译。
当您对Python解释器过程有更深入的理解时,这些核心概念在第2部分将是至关重要的。
-跟进-