【CSDN软件工程师能力认证学习精选】教你阅读 Cpython 的源码

CSDN软件工程师能力认证(以下简称C系列认证)是由中国软件开发者网CSDN制定并推出的一个能力认证标准。C系列认证历经近一年的实际线下调研、考察、迭代、测试,并梳理出软件工程师开发过程中所需的各项技术技能,结合企业招聘需求和人才应聘痛点,基于公开、透明、公正的原则,甑别人才时确保真实业务场景、全部上机实操、所有过程留痕、存档不可篡改。

我们每天将都会精选CSDN站内技术文章供大家学习,帮助大家系统化学习IT技术。

前言

 

这篇文章很长但是很有用,如果你决定要学习 Cpython,那么希望你能看下去,你会发现这是一份不错的学习资料。

第一部分 介绍 Cpython

我们平时说的 Python,其实大多都是指的 Cpython,CPython 是众多 Python 中的一种,除此之外还有 Pypy,Jpython 等。CPython 同样的作为官方使用的 Python 版本,以及网上的众多案例。所以,这里我们主要说的是 Cpython。

源代码中有什么?

CPython 源代码分发包含各种工具,库和组件。我们将在本文中探讨这些内容。

 

 

git clone https://github.com/python/cpython
cd cpython
git checkout v3.8.0b3 #切换我们需要的分支

 

注意:如果你没有 Git,可以直接从 GitHub 网站下载 ZIP 文件中的源代码。

 

 

cpython/
│
├── Doc      ← 源代码文档说明
├── Grammar  ← 计算机可读的语言定义
├── Include  ← C 语言头文件(头文件中一般放一些重复使用的代码)
├── Lib      ← Python 写的标准库文件
├── Mac      ← Mac 支持的文件
├── Misc     ← 杂项
├── Modules  ← C 写的标准库文件
├── Objects  ← 核心类型和对象模块
├── Parser   ← Python 解析器源码
├── PC       ← Windows 编译支持的文件
├── PCbuild  ← 老版本的 Windows 系统 编译支持的文件
├── Programs ← Python 可执行文件和其他二进制文件的源代码
├── Python   ← CPython  解析器源码
└── Tools    ← 用于构建或扩展 Python 的独立工具

 

接下来,我们将从源代码中编译 CPython。

在 macOS 上编译 CPython 非常简单。在终端内,运行以下命令即可安装 C 编译器和工具包:

$ xcode-select --install

此命令将弹出一个提示,下载并安装一组工具,包括 Git,Make 和 GNU C 编译器。

$ brew install openssl xz zlib

现在你已拥有依赖项,你可以运行 Cpython 目录下的 configure 脚本:

 

 

$ CPPFLAGS="-I$(brew --prefix zlib)/include" \
 LDFLAGS="-L$(brew --prefix zlib)/lib" \
 ./configure --with-openssl=$(brew --prefix openssl) --with-pydebug

 

上面的安装命令中,CPPFLAGS 是 c 和 c++ 编译器的选项,这里指定了 zlib 头文件的位置,LDFLAGS 是 gcc 等编译器会用到的一些优化参数,这里是指定了 zlib 库文件的位置,(brew --prefix openssl) 这一部分的意思是在终端里执行括号里的命令,显示 openssl 的安装路径,可以事先执行括号里的命令,用返回的结果替换 (brew --prefix openssl),效果是一样的,每一行行尾的反斜杠可以使换行时先不执行命令,而是把这三行内容当作一条命令执行。

运行完上面命令以后在存储库的根目录中会生成一个 Makefile,你可以使用它来自动化构建过程。./configure步骤只需要运行一次。

$ make -j2 -s

-j2 标志允许 make 同时运行 2 个作业。如果你有 4 个内核,则可以将其更改为 4. -s 标志会阻止 Makefile 将其运行的每个命令打印到控制台。你可以删除它,输出的东西太多了。在构建期间,你可能会收到一些错误,在摘要中,它会通知你并非所有包都可以构建。

 

 

$ ./python.exe
Python 3.8.0b3 (tags/v3.8.0b3:4336222407, Aug 21 2019, 10:00:03) 
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 

 

(其实最新的已经到 Python3.9 了,我编译了一下效果如下)

640?wx_fmt=jpeg

编译器做了什么?

编译器的目的就是将一种语言转为另外一种语言。可以把编译的过程比作翻译,把英语里的“Hello”,翻译成中文的「你好」。

一些编译器将代码编译成只有机器看懂的机器代码,可以直接在系统上进行执行。其他编译器将编译成中间语言,由虚拟机执行。

Python 代码不会编译成机器代码。

为什么 CPython 是用 C 而不是 Python 编写的?

CPython 中的 C 是对 C 编程语言的引用,暗示这个 Python 发行版是用 C 语言编写的。

那么为什么 CPython 是用 C 而不是 Python 编写的?

答案就在于编译器的工作原理。

  • 自托管编译器是用它们编译的语言编写的编译器,例如 Go 编译器。

  • 源到源编译器是用另一种已经有编译器的语言编写的编译器。

CPython 保留了它的 C 的特性:许多标准库模块(如 ssl 模块或 sockets 模块)都是用 C 语言编写的,用于访问低级操作系统 API。

还有一个就是 Jython。Jython 是用 Java 编写的,从 Python 源代码编译成 Java 字节码。与 CPython 可以轻松导入 C 库并从 Python 中使用它们一样,Jython 使得导入和引用 Java 模块和类变得容易。

Python 语言规范

CPython 源代码中包含的是 Python 语言的定义。这是所有 Python 解释器使用的参考规范。该规范采用人类可读和机器可读的格式。文档内部详细说明了 Python 语言,允许的内容以及每个语句的行为方式。

文档

位于Doc/reference目录内的是reStructuredText文件解释了 Python 语言中每个功能属性。这构成了docs.python.org上的官方 Python 参考指南。

 

 

cpython/Doc/reference
|
├── compound_stmts.rst
├── datamodel.rst
├── executionmodel.rst
├── expressions.rst
├── grammar.rst
├── import.rst
├── index.rst
├── introduction.rst
├── lexical_analysis.rst
├── simple_stmts.rst
└── toplevel_components.rst

 

compound_stmts.rst文件中,你可以看到一个定义 with 语句的简单示例。with 语句可以在 Python 中以多种方式使用,最简单的是上下文管理器的实例化和嵌套的代码块:

 
with x():
   ...

你可以使用 as 进行重命名

 

 

with x() as y:
   ...

 

你还可以链式的同时定义多个

 

 

with x() as y, z() as jk:
   ...

 

接下来,我们将探索 Python 语言的计算机可读文档。

Grammar

该文档包含人类可读规范和存放在单个文件Grammar/Grammar中的机器可读规范。Noam Chomsky’s work on Syntactic Structures中受到启发的。

  • *重复

  • +至少重复一次

  • []为可选部分

  • |任选一个

  • ()用于分组

 

 

.. productionlist::
   with_stmt: "with" `with_item` ("," `with_item`)* ":" `suite`
   with_item: `expression` ["as" `target`]

 

引号中的内容都是字符串,这是一中关键字的定义方式。所以 with_stmt 指定为:with单词开头test和(可选)as 表达式。:结尾

  • suite是指具有一个或多个语句的代码块。

  • test是指一个被评估的简单语句。

  • expr指的是一个简单的表达式

如果你想看一个最近如何使用语法的例子,例如在 PEP572 中,:=运算符被添加到语法文件中。

 

 

  ATEQUAL                 '@='
  RARROW                  '->'
  ELLIPSIS                '...'
+ COLONEQUAL              ':='
  OP
  ERRORTOKEN

 

使用 pgen

Grammar 文件本身不会被 Python 编译器使用。

注意:pgen 应用程序在 Python 3.8 中从 C 重写为纯 Python。

为了查看 pgen 的运行情况,让我们改变 Python 语法的一部分。并重新编译运行 Python。pass_stmt,然后看到下面这样

pass_stmt: 'pass'

我们修改一下,改为下面这样

pass_stmt: 'pass' | 'proceed'

在 Cpython 的根目录使用make regen-grammar命令来运行pgen重新编译 Grammar 文件。Include/graminit.hPython/graminit.c文件:

 

 

# Regenerate Include/graminit.h and Python/graminit.c
# from Grammar/Grammar using pgen
PYTHONPATH=. python3 -m Parser.pgen ./Grammar/Grammar \
        ./Grammar/Tokens \
        ./Include/graminit.h.new \
        ./Python/graminit.c.new
python3 ./Tools/scripts/update_file.py ./Include/graminit.h ./Include/graminit.h.new
python3 ./Tools/scripts/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, Aug 21 2019, 10:00:03) 
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> def example():
...    proceed
... 
>>> example()

 

下面是我运行结果,很有意思居然没有出错。

640?wx_fmt=jpeg

 

Tokens

与 Grammar 文件夹中的语法文件一起是一个 Tokens 文件,它包含在解析树中作为叶节点找到的每个唯一类型,稍后我们将深入介绍解析器树。每个 token 还具有名称和生成的唯一 ID,这些名称用于简化在 tokenizer 中引用。

注意:Tokens 文件是 Python 3.8 中的一项新功能。

例如,左括号称为 LPAR,分号称为 SEMI。

 

 

LPAR                    '('
RPAR                    ')'
LSQB                    '['
RSQB                    ']'
COLON                   ':'
COMMA                   ','
SEMI                    ';'

 

与语法文件一样,如果更改 Tokens 文件,则需要再次运行 pgen。

 

 

# Hello world!
def my_function():
   proceed

 

然后通过名为 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:            NAME           'proceed'         
3,7-3,8:            NEWLINE        '\n'           
4,0-4,0:            DEDENT         ''             
4,0-4,0:            ENDMARKER      ''   

 

640?wx_fmt=jpeg

Lib/tokenize.py中。

 

重要提示:CPython 源代码中有两个 tokenizers:一个用 Python 编写,上面演示的这个,另一个是用 C 语言编写的。用 Python 编写的被用作实用程序,而用 C 编写的被用于 Python 编译器。但是,它们具有相同的输出和行为。用 C 语言编写的版本是为性能而设计的,Python 中的模块是为调试而设计的。

要查看 C 语言的的 tokenizer 的详细内容,可以使用-d 标志运行 Python。

./python.exe -d test_tokens.py

得到如下结果

 

 

Token NAME/'def' ... It's a keyword
 DFA 'file_input', state 0: Push 'stmt'
 DFA 'stmt', state 0: Push 'compound_stmt'
 DFA 'compound_stmt', state 0: Push 'funcdef'
 DFA 'funcdef', state 0: Shift.
Token NAME/'my_function' ... It's a token we know
 DFA 'funcdef', state 1: Shift.
Token LPAR/'(' ... It's a token we know
 DFA 'funcdef', state 2: Push 'parameters'
 DFA 'parameters', state 0: Shift.
Token RPAR/')' ... It's a token we know
 DFA 'parameters', state 1: Shift.
  DFA 'parameters', state 2: Direct pop.
Token COLON/':' ... It's a token we know
 DFA 'funcdef', state 3: Shift.
Token NEWLINE/'' ... It's a token we know
 DFA 'funcdef', state 5: [switch func_body_suite to suite] Push 'suite'
 DFA 'suite', state 0: Shift.
Token INDENT/'' ... It's a token we know
 DFA 'suite', state 1: Shift.
Token NAME/'proceed' ... It's a keyword
 DFA 'suite', state 3: Push 'stmt'
...
  ACCEPT.

 

在输出中,您可以看到它突出显示为关键字。在下一章中,我们将看到如何执行 Python 二进制文件到达 tokenizer 以及从那里执行代码会发生什么。现在您已经概述了 Python 语法以及 tokens 和语句之间的关系,有一种方法可以将 pgen 输出转换为交互式图形。

640?wx_fmt=jpeg

看不清没关系,用于生成此图的 Python 包(instaviz)将在后面的章节中介绍。这里先做了解。

 

Python 中的内存管理

在本文中,你将看到对 PyArena 对象的引用。

arena是 CPython 的内存管理结构之一。代码在Python/pyarena.c中其中包含了 C 的内存分配和解除分配的方法。

在编写的 C 程序中,开发人员应在写入数据之前为数据结构分配内存。此分配将内存标记为属于操作系统的进程。当不再使用已分配的内存并将其返回到操作系统的可用内存块表时,开发人员也可以解除分配或“释放”它们。如果进程为一个变量分配内存,比如在函数或循环中,当该函数完成时,内存不会自动返回给 C 中的操作系统。因此,如果它未在 C 代码中显式释放,则会导致内存泄漏。每次该函数运行时,该过程将继续占用更多内存,直到最终,系统耗尽内存并崩溃!Python 将这一责任从程序员手中夺走,并使用两种算法:引用计数器和垃圾收集器。每当解释器被实例化时,PyArena方法创建并附加解释器中的一块内存区域。在 CPython 解释器的生命周期中,arenas可以被分配。它们与链表相关联。

arenas将 Python 对象的指针列表存储为PyListObject方法。每当创建一个新的 Python 对象时,都会使用PyArena_AddPyObject方法添加指向它的指针。arenas列表 a_objects 中。PyArena方法提供第二个功能,即分配和引用原始内存块列表。例如,如果添加了数千个附加值,C 代码中PyList将需要额外的内存。但是PyList不直接分配内存。该对象通过从PyObject调用具有所需内存大小的PyArena_MallocPyArena获取原始内存块。此任务在Objects/oballoc.c中的完成。在对象分配模块中,可以为 Python 对象分配,释放和重新分配内存。已分配块的链接列表存储在arenas内,因此当解释器停止时,可以使用PyArena_Free一次解除所有托管内存块的释放。

PyListObject为例,如果你使用.append()一将个对象放到 Python 列表的末尾,就不需要重新分配内存了,而是使用现有列表中内存。.append()方法调用list_resize()来处理列表的内存分配。每个列表对象都保留已分配内存量的列表。如果要追加的项目将适合现有的可用内存,则只需添加即可。如果列表需要更多内存空间,则会进行扩展。列表的长度扩展为 0,4,8,16,25,35,46,58,72,88。

调用PyMem_Realloc可以扩展列表中分配的内存。PyMem_Reallocpymalloc_realloc的 API 包装器。Python 还有一个 C 调用malloc的特殊包装器,它设置内存分配的最大大小以帮助防止缓冲区溢出错误(参见 PyMem_RawMalloc)。

  • 原始内存块的分配是通过PyMem_RawAlloc完成的。

  • Python 对象的指针存储在PyArena中。

  • PyArena还存储了已分配内存块的链表。

引用计数

要在 Python 中创建变量并赋值,变量名必须为一。

my_variable = 180392

只要在 Python 中为变量赋值,就会在 locals 和 globals 范围内检查变量的名称,以查看它是否已存在。因为 my_variable 不在 locals()或 globals()字典中,所以创建了这个新对象,并将该值指定为数字常量 180392。现在有一个对 my_variable 的引用,因此 my_variable 的引用计数器增加 1。Py_INCREFPy_DECREFceval.c文件中。我们将在本文后面详细介绍。

每当调用Py_DECREF并且计数器变为 0 时,就会调用PyObject_Free函数。对于该对象,会为所有已分配的内存调用PyArena_Free

垃圾收集

CPython 的垃圾收集器默认启用,发生在后台,用于释放已不再使用的对象的内存。arena和垃圾收集器 gc 模块连接。

 

 

>>> import gc
>>> gc.set_debug(gc.DEBUG_STATS)

 

这将在运行垃圾收集器时打印统计信息。get_threshold来获取运行垃圾收集器的阈值:

 

 

>>> gc.get_threshold()
(700, 10, 10)

 

还可以获取当前阈值计数:

 

 

>>> gc.get_count()
(688, 1, 1)

 

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

 

 

>>> gc.collect()
24

 

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

结论

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

 

 

关于CSDN软件工程师能力认证

      CSDN软件工程师能力认证(以下简称C系列认证)是由中国软件开发者网CSDN制定并推出的一个能力认证标准。C系列认证历经近一年的实际线下调研、考察、迭代、测试,并梳理出软件工程师开发过程中所需的各项技术技能,结合企业招聘需求和人才应聘痛点,基于公开、透明、公正的原则,甑别人才时确保真实业务场景、全部上机实操、所有过程留痕、存档不可篡改。C系列认证的宗旨是让一流的技术人才凭真才实学进大厂拿高薪,同时为企业节约大量招聘与培养成本,使命是提升高校大学生的技术能力,为行业提供人才储备,为国家数字化战略贡献力量。

 

了解详情可点击:CSDN软件工程师能力认证介绍

 

本文出处:https://blog.csdn.net/muzico425/article/details/100070351?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522161421800016780261942815%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=161421800016780261942815&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~sobaiduend~default-3-100070351.pc_search_result_before_js&utm_term=CPython&spm=1018.2226.3001.4187

 

  • 0
    点赞
  • 0
    评论
  • 2
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值