当前位置:崇尚新闻网 > 独家 > 正文

cpython 教你阅读 Cpython 的源码(一)

导语: 正文:12920字,7位数 预计阅读时间:33分钟 来源:https://realpython.com/cpython-source-code-guide, 译者:陈翔安 就像题目一样,这篇文章是教你关于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)]在达尔文

键入“帮助”、“版权”、“学分”或“许可证”以获取更多信息。

&gt。&gt。&gt。

(其实最新的一个已经到了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)]在达尔文

键入“帮助”、“版权”、“学分”或“许可证”以获取更多信息。

&gt。&gt。&gt。失败示例:

...进行

...

&gt。&gt。&gt。例子

以下是我手术的结果。有趣的是没有出错。

接下来,我们将讨论令牌文件及其与语法的关系。记号

与语法文件夹中的语法文件一起,它是一个标记文件,包含解析树中作为叶节点的每个唯一类型。稍后我们将深入介绍解析器树。每个令牌也有一个名称和一个生成的唯一标识,用于简化令牌化器中的引用。

注意:令牌文件是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模块:

&gt。&gt。&gt。导入gc

&gt。&gt。&gt。gc.set_debug(gc。调试_统计)

这将在您运行垃圾收集器时打印统计信息。

您可以通过调用get_threshold来获取运行垃圾收集器的阈值:

&gt。&gt。&gt。gc.get_threshold

( 700, 10, 10)

您还可以获得当前阈值计数:

&gt。&gt。&gt。gc.get_count

( 688, 1, 1)

最后,您可以手动运行收集算法:

&gt。&gt。&gt。gc.collect

24

这个调用收集在Modules/gcmodule.c文件中,该文件包含垃圾收集器算法的实现。

结论

在第1部分中,我们介绍了源代码库的结构,如何从源代码和Python语言规范进行编译。

当您对Python解释器过程有更深入的理解时,这些核心概念在第2部分将是至关重要的。

-跟进-

免责申明:以上内容属作者个人观点,版权归原作者所有,不代表崇尚新闻网立场!登载此文只为提供信息参考,并不用于任何商业目的。如有侵权或内容不符,请联系我们处理,谢谢合作!
当前文章地址:https://www.csxming.com/dujia/401314.html 感谢你把文章分享给有需要的朋友!
上一篇:魏晋名士 癫狂:魏晋名士与药和酒 下一篇: sudan 埃及和苏丹领土争议:为何有块地谁都不想要,另一块地却抢着要?