教你阅读CPython的源码
以下文章来源于Python学习开发 ,作者陈祥安
就如同题目一样,这篇文章就是教你了解CPython的一篇文章。因为内容太长了打算先分开写,后期看看再合并。
前言
这篇文章很长但是很有用,如果你决定要学习 CPython,那么希望你能看下去,你会发现这是一份不错的学习资料。
这篇文章总共分为 5 部分,你可以根据自己的时候合理的安排阅读时间。每一部分都要花一定的时间,通过自己去研究这里面的一些案例,你会感到一种成就感,因为你掌握了 Python 的核心概念,这使得你成为一名更好的 Python 程序员。
第一部分 介绍 CPython
我们平时说的 Python,其实大多都是指的 CPython,CPython 是众多 Python 中的其中一种,初次之外还有 Pypy,Jpython 等。CPython 同样的作为官方使用的 Python 版本,以及网上的众多案例。所以,这里我们主要说的是 CPython。
注意:本文是针对 CPython 源代码的 3.8.0b3 版编写的。
▌源代码中有什么?
CPython 源代码分发包含各种工具,库和组件。我们将在本文中探讨这些内容。
首先,我们将重点关注编译器。先从 git 上下载 CPython 源代码.
cd cpython
git checkout v3.8.0b3 #切换我们需要的分支
│
├── Doc ← 源代码文档说明
├── Grammar ← 计算机可读的语言定义
├── Include ← C 语言头文件(头文件中一般放一些重复使用的代码)
├── Lib ← Python 写的标准库文件
├── Mac ← Mac 支持的文件
├── Misc ← 杂项
├── Modules ← C 写的标准库文件
├── Objects ← 核心类型和对象模块
├── Parser ← Python 解析器源码
├── PC ← Windows 编译支持的文件
├── PCbuild ← 老版本的 Windows 系统 编译支持的文件
├── Programs ← Python 可执行文件和其他二进制文件的源代码
├── Python ← CPython 解析器源码
└── Tools ← 用于构建或扩展 Python 的独立工具
LDFLAGS="-L$(brew --prefix zlib)/lib" \
./configure --with-openssl=$(brew --prefix openssl) --with-pydebug
构建将花费几分钟并生成一个名为 python.exe 的二进制文件。每次改动源代码,都需要重新运行 make 进行编译。
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.
>
自托管编译器是用它们编译的语言编写的编译器,例如 Go 编译器。
源到源编译器是用另一种已经有编译器的语言编写的编译器。这也就意味着如果从头开始编写新的编程语言,则需要一个可执行的应用程序来编译你的编译器!你就需要一个编译器来执行任何操作,因此在开发新语言时,它们通常首先用较旧的,更成熟的语言编写。同时节省时间和学习成本。
在目录中是你需要了解整个语言,结构和关键字的文件:
|
├── 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
...
...
...
*重复 +至少重复一次 []为可选部分 |任选一个 ()用于分组
with_stmt: "with" `with_item` ("," `with_item`)* ":" `suite`
with_item: `expression` ["as" `target`]
with单词开头 接下来是 with_item,它是一个test和(可选)as 表达式。 多个项目之间使用逗号进行间隔 以字符:结尾 其次是 suite。
suite是指具有一个或多个语句的代码块。 test是指一个被评估的简单语句。 expr指的是一个简单的表达式
RARROW '->'
ELLIPSIS '...'
+ COLONEQUAL ':='
OP
ERRORTOKEN
# 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
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
> def example():
... proceed
...
> example()
下面是我运行结果,很有意思居然没有出错。
接下来,我们将探讨 Tokens 文件及其与 Grammar 的关系。
▌Tokens
与 Grammar 文件夹中的语法文件一起是一个 Tokens 文件,它包含在解析树中作为叶节点找到的每个唯一类型,稍后我们将深入介绍解析器树。每个 token 还具有名称和生成的唯一 ID,这些名称用于简化在 tokenizer 中引用。
注意:Tokens 文件是 Python 3.8 中的一项新功能。
例如,左括号称为 LPAR,分号称为 SEMI。
你将在本文后面看到这些标记:
RPAR ')'
LSQB '['
RSQB ']'
COLON ':'
COMMA ','
SEMI ';'
def my_function():
proceed
然后通过名为 tokenize 的标准库中内置的模块传递此文件。你将按行和字符查看令牌列表。使用-e 标志输出确切的令牌名称:
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 ''
在输出中,第一列是行/列坐标的范围,第二列是令牌的名称,最后一列是令牌的值。
在输出中,tokenize 模块隐含了一些不在文件中的标记。
utf-8 的 ENCODING 标记,末尾有一个空行,DEDENT 关闭函数声明,ENDMARKER 结束文件。tokenize 模块是用纯 Python 编写的,位于CPython 源代码中的Lib/tokenize.py中。
重要提示:CPython 源代码中有两个 tokenizers:一个用 Python 编写,上面演示的这个,另一个是用 C 语言编写的。用 Python 编写的被用作实用程序,而用 C 编写的被用于 Python 编译器。但是,它们具有相同的输出和行为。用 C 语言编写的版本是为性能而设计的,Python 中的模块是为调试而设计的。
要查看 C 语言的的 tokenizer 的详细内容,可以使用-d 标志运行Python。
使用之前创建的 test_tokens.py 脚本,使用以下命令运行它:
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.
原始内存块的分配是通过PyMem_RawAlloc完成的。 Python 对象的指针存储在PyArena中。 PyArena还存储了已分配内存块的链表。
只要在 Python 中为变量赋值,就会在 locals 和 globals 范围内检查变量的名称,以查看它是否已存在。因为 my_variable 不在 locals()或 globals()字典中,所以创建了这个新对象,并将该值指定为数字常量180392。现在有一个对 my_variable 的引用,因此 my_variable 的引用计数器增加 1。
你可以在 CPython 的 C 源代码中看到函数Py_INCREF和Py_DECREF。
这两个函数分别是对该对象的递增和递减做引用计数。当变量超出声明范围时,对对象的引用会递减。Python 中的范围可以指代函数或方法,生成式或 lambda 函数。这些是一些更直观的范围,但还有许多其他隐式范围,比如将变量传递给函数调用。递增和递减引用的处理在 CPython 编译器和核心执行循环ceval.c文件中。我们将在本文后面详细介绍。
每当调用Py_DECREF并且计数器变为 0 时,就会调用PyObject_Free函数。对于该对象,会为所有已分配的内存调用PyArena_Free。
▌垃圾收集
CPython 的垃圾收集器默认启用,发生在后台,用于释放已不再使用的对象的内存。
因为垃圾收集算法比引用计数器复杂得多,所以它不会一直发生,否则会消耗大量的 CPU 资源。经过一定数量的操作后,它会定期发生。CPython 的标准库附带了一个 Python 模块,用于与arena和垃圾收集器 gc 模块连接。
以下是在调试模式下使用 gc 模块的方法:
> gc.set_debug(gc.DEBUG_STATS)
(700, 10, 10)
(688, 1, 1)
24
这将调用Modules/gcmodule.c文件中的collect(),该文件包含垃圾收集器算法的实现。
结论
在第 1 部分中,我们介绍了源代码库的结构,如何从源代码编译以及 Python 语言规范。
当你深入了解 Python 解释器过程时,这些核心概念在第 2 部分中将是至关重要的。
原文地址:
https://realpython.com/cpython-source-code-guide
(*本文为AI科技大本营转载文章,转载请联系原作者)
◆
精彩推荐
◆
「2019 AI开发者大会」 除了邀请国内外一线公司重磅嘉宾外,还邀请到了亚马逊首席科学家@李沐,他将于9月5日亲授「深度学习实训营」,通过动手实操,帮助开发者全面了解深度学习的基础知识和开发技巧。原价1099元,目前福利价199元!!且现场赠送价值85元《动手学深度学习》一本。
社群福利
扫码添加小助手,回复:大会,加入2019 AI开发者大会福利群,每周更新技术福利,还有不定期的抽奖活动~推荐阅读