You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
编译器引导的一个很好的例子是 Go 编程语言。
第一个 Go 编译器是用 C 编写的,然后一旦 Go 可以编译了,就用 Go 重写编译器。
CPython 保留了 C 语言的传统;许多标准库模块,如 ssl 模块或套接字模块,都是用 C 编写的,用于访问低级操作系统 API。
Windows 和 Linux 内核中用于创建网络套接字、 使用文件系统或 与显示器交互的 API 都是用 C 编写的。
Python 的可扩展性层专注于 C 语言是有意义的。
编译器的目的是将一种语言转换成另一种语言。把编译器想象成一个翻译器。
比如你会雇一个翻译来听你说英语,然后翻译成日语。
为此,翻译人员必须了解源语言和目标语言的语法结构。
有些编译器会编译成低级机器码,可以直接在系统上执行。其他编译器会编译成一种中间语言,由虚拟机执行。
选择编译器时的一个考虑因素是系统可移植性要求。
Java
和 .NET CLR
将编译成一种中间语言,以便编译后的代码可以跨多个系统架构移植。
C、Go、C++ 和 Pascal 将编译成可执行的二进制文件。此二进制文件是为编译它的平台构建的。
Python 应用程序通常作为源代码分发。Python 解释器的作用是将Python源代码进行转换并一步执行。
CPython 运行时在第一次运行时会编译你的代码。这一步对普通用户是不可见的。
Python 代码不会被编译成机器码;它被编译成一种称为 字节码 的低级中间语言。
此字节码存储在
.pyc
文件中并缓存以供执行。如果在不更改源代码的情况下两次运行同一个 Python 应用程序,则第二次执行速度会更快。
这是因为它加载编译后的字节码而不是每次都重新编译。
为什么 CPython 是用 C 而不是用 Python 编写
CPython 中的 C 是对 C 编程语言的引用,这意味着这个 Python 发行版是用 C 语言编写的。
这种说法大多是正确的:CPython 中的编译器是用纯 C 编写的。
但是,许多标准库模块是用纯 Python 或 C 和 Python 组合编写的。
那么为什么 CPython 编译器是用 C 而不是 Python 编写的呢?
答案在于编译器的工作方式。 有两种类型的编译器:
自举编译器
是用它们编译的语言编写的编译器,例如 Go 编译器。这是通过称为引导的过程完成的。
源到源编译器
是用另一种已经有编译器的语言编写的编译器。
如果你要从头开始编写新的编程语言,则需要一个可执行应用程序来编译你的编译器!
你需要一个编译器来执行任何事情,所以当开发新语言时,它们通常首先用更老的、更成熟的语言编写。
还有一些可用的工具可以读取语言规范并创建解析器。
流行的编译器-编译器(compiler-compilers)包括 GNU Bison、Yacc 和 ANTLR。
编译器引导的一个很好的例子是 Go 编程语言。
第一个 Go 编译器是用 C 编写的,然后一旦 Go 可以编译了,就用 Go 重写编译器。
CPython 保留了 C 语言的传统;许多标准库模块,如 ssl 模块或套接字模块,都是用 C 编写的,用于访问低级操作系统 API。
Windows 和 Linux 内核中用于创建网络套接字、
使用文件系统或
与显示器交互的 API 都是用 C 编写的。
Python 的可扩展性层专注于 C 语言是有意义的。
有一个用 Python 编写的 Python 编译器,称为 PyPy。
PyPy 的标志是一个 衔尾蛇,代表编译器的自举性质。
Python 交叉编译器的另一个示例是 Jython。Jython 是用 Java 编写的,从 Python 源代码编译成 Java 字节码。
与 CPython 可以轻松导入 C 库并从 Python 中使用它们一样,Jython 可以轻松导入和引用 Java 模块和类。
创建编译器的第一步是定义语言。 例如,一下不是有效的 Python:
编译器在尝试执行之前需要严格的语言语法结构规则。
Python 语言规范
CPython 源代码中包含 Python 语言的定义。这个文档是所有 Python 解释器使用的参考规范。
该规范采用人类可读和机器可读的格式。文档里面是对 Python 语言的详细解释。包含允许的内容以及每个语句的行为方式。
语言文档
位于
Doc/reference
目录中的是 Python 语言中每个功能的 reStructured-Text 解释。这些文件构成了 docs.python.org/3/reference 上的官方 Python 参考指南。
目录里面是你需要了解整个语言、结构和关键字的文件:
一个例子
在
Doc/reference/compound_stmts.rst
,你可以看到一个定义with
语句的简单示例。with
语句有多种形式,最简单的是上下文管理器的实例化和嵌套的代码块:你可以使用
as
关键字将结果分配给变量:你还可以使用逗号将上下文管理器链接在一起:
文档包含语言的人类可读规范,机器可读规范包含在单个文件
Grammar Grammar
中。语法文件
语法文件以一种称为巴科斯范式 (BNF) 的上下文符号编写。
巴科斯范式不是 Python 特有的,通常用作许多其他语言中的语法符号。
编程语言中语法结构的概念受到 Noam Chomsky 在 1950 年代关于句法结构的工作的启发!
Python 的语法文件使用扩展巴科斯范式(EBNF)规范和正则表达式语法。因此,在语法文件中,你可以使用:
*
用于重复+
至少重复一次[]
用于可选部分|
对于替代品()
用于分组例如,考虑如何定义一杯咖啡:
在 EBNF 中定义的咖啡订单可能如下所示:
在本章中,语法是用铁路图形象化的。 这张图是咖啡语句的铁路图:
在铁路图中,每个可能的组合必须从左到右排成一条线。 可选语句可以被绕过,有些语句可以形成循环。
如果在语法文件中搜索
with_stmt
,可以看到定义:引号中的任何内容都是字符串文字,称为终端(terminal)。终端是识别关键字的方式。
with_stmt
指定为:with
开始with_item
,它可以是test
,和(可选的)as
以及一个表达式expr
with_item
,每个都用逗号隔开:
结尾suite
在这两行中引用了其他三个定义:
•
suite
是指包含一个或多个语句的代码块•
test
指的是一个被评估的简单的语句•
expr
指的是一个简单的表达式在铁路图中可视化,
with
语句如下所示:作为一个更复杂的例子,
try
语句定义为:try
语句有两种用途:try
和一个或多个except
子句,然后是一个可选的else
,然后是一个可选的finally
try
和只有一个finally
语句或者,在铁路图中可视化:
try
语句是更复杂结构的一个很好的例子。如果你想详细了解 Python 语言,语法在
Grammar/Grammar
中定义。使用解析器生成器(The Parser Generator)
Python 编译器从不使用语法文件本身。相反,解析器表由解析器生成器创建。
如果对语法文件进行更改,则必须重新生成解析器表并重新编译 CPython。
解析器表是潜在解析器状态的列表。当解析树变得复杂时,它们确保语法不会有歧义。
解析器生成器
解析器生成器的工作原理是将 EBNF 语句转换为非确定性有限自动机 (Non-deterministic Finite Automaton,NFA)。
NFA 状态和转换被解析并合并为一个确定性有限自动机 (Deterministic Finite Automaton,DFA)。
DFA 被解析器用作解析表。这种技术是在斯坦福大学形成的,并在 1980 年代开发,就在 Python 出现之前。
CPython 的解析器生成器
pgen
是 CPython 项目独有的。pgen
应用程序在 Python 3.8 中从 C 重写为 Python,在文件Parser/pgen/pgen.py
中。它可通过以下执行:
它通常从构建脚本执行,而不是直接执行。
DFA 和 NFA 没有视觉输出,但有一个带有有向图输出的 CPython 分支。
decorator
语法在Grammar/Grammar
中定义为:解析器生成器创建了一个包含 11 个状态的复杂 NFA 图。每个状态都用数字表示(在语法中提示它们的名称)。
状态转移被称为“弧”。
DFA 比 NFA 更简单,路径减少了:
NFA 和 DFA 图仅用于调试复杂语法的设计。
我们将使用铁路图代替 DFA 或 NFA 图来表示语法。例如,此图表示
decorator
语句可以采用的路径:重新生成语法
要查看
pgen
的运行情况,让我们更改部分 Python 语法。在
Grammar/Grammar
中搜索pass_stmt
以查看pass
语句的定义:通过添加选择
|
和proceed
字面量,更改该行以接受终端(关键字)'pass'
或'proceed'
作为关键字:接下来,通过运行
pgen
重建语法文件。CPython 带有脚本来自动化pgen
。在 macOS 和 Linux 上,运行
make regen-grammar
:对于 Windows,从 PCBuild 目录调出命令行并使用
--regen
标志运行build.bat
:> build.bat --regen
你应该会看到一个输出,显示新的
Include/graminit.h
和Python/graminit.c
文件已重新生成。使用重新生成的解析器表,当你重新编译 CPython 时,它将使用新语法。
如果代码编译成功,你可以执行新的 CPython 二进制文件并启动 REPL。
在 REPL 中,你现在可以尝试定义一个函数。不要使用 pass 语句,
而是使用你编译到 Python 语法中的
proceed
关键字替代pass
:恭喜,你已经更改了 CPython 语法并编译了你自己的 CPython 版本。
接下来,我们将探索标记(tokens)及其与语法的关系。
标记(Tokens)
除了
Grammar
文件夹中的语法文件之外,还有Grammar/Tokens
文件,其中包含在分析树中作为叶节点找到的每个唯一类型。每个标记还有一个名称和一个生成的唯一 ID。
名称用于使在分词器(tokenizer)中更容易引用。
例如,左括号称为
LPAR
,分号称为称为SEMI
。 你将在本书后面看到这些标记:和
Grammar
文件一样,如果你修改了Grammar/Tokens
文件,你需要重新运行pgen
。要查看操作中的标记,你可以使用 CPython 中的
tokenize
模块。cpython-book-samples/13/test_tokens.py
:将
test_tokens.py
文件输入到标准库中内置的名为tokenize
的模块中。你将按行和字符看到标记列表。使用
-e
标志输出确切的标记名称:在输出中,第一列是行/列坐标的范围,第二列是标记的名称,最后一列是标记的值。
在输出中,
tokenize
模块隐含了一些标记:utf-8
的ENCODING
标记DEDENT
关闭函数声明ENDMARKER
结束文件最佳做法是在 Python 源文件的末尾有一个空行。如果省略它,CPython 会为你添加它。
tokenize
模块是用纯 Python 编写的,位于Lib/tokenize.py
中。要查看 C 分词器的详细读数,您可以使用
-d
标志运行 Python。使用之前创建的
test_tokens.py
脚本,使用以下命令运行它:在输出中,你可以看到它突出显示了作为关键字的
proceed
。在下一章中,我们将看到执行 Python 二进制文件是如何到达分词器的,以及从那里执行代码时会发生什么。
一个更复杂的例子
添加
proceed
作为pass
的替代关键字是一个简单的更改,解析器生成器将
'proceed'
作为pass_stmt
标记的文字进行匹配。这个新关键字无需对编译器进行任何更改即可工作。
在实践中,对语法的大多数更改都更加复杂。
Python 3.8 引入了赋值表达式,格式为
:=
。赋值表达式既为名称赋值,又返回命名变量的值。受在 Python 语言中添加赋值表达式影响的语句之一是
if
语句。在 3.8 之前,
if
语句定义为:if
后跟test
,然后是:
suite
)elif
语句,后跟test
、一个:
和suite
else
语句,后跟一个:
和一个suite
在语法中,这表示为:
可视化之后看起来像:
为了支持赋值表达式,更改需要向后兼容。 因此,在
if
语句中使用:=
必须是可选的。if
语句中使用的test
标记类型在许多语句之间是通用的。例如,assert
语句后跟一个test
(然后是可选的第二个test
)。在 3.8 中添加了替代
test
标记类型,以便语法可以规定哪些语句应该支持赋值表达式,哪些不应该支持。这个称为
namedexpr_test
,在Grammer
中定义为:或者,在铁路图中可视化为:
if
语句的新语法已更改为用namedexpr_test
替换test
:在铁路图中可视化:
为了区分
:=
和现有的COLON
(:
) 和EQUAL
(=
) 标记,将以下标记也添加到Grammar/Tokens
中:这不是支持赋值表达式所需的唯一更改。 如 Pull Request 中所示,这一变化改变了 CPython 编译器的许多部分。
总结
在本章中,你已经了解了 Python 语法定义和解析器生成器。
在下一章中,你将扩展该知识以构建更复杂的语法功能,即“几乎等于”运算符。
在实践中,必须仔细考虑和讨论对 Python 语法的更改。审查水平有两个原因:
如果 Python 核心开发人员提议对语法进行更改,则必须将其作为 Python 增强提案 (PEP) 提出。
所有 PEP 都在 PEP 索引上进行编号和索引。
PEP 5 记录了语言发展的指南,并指定必须在 PEP 中提出更改。
成员还可以通过 python-ideas 邮件列表建议对核心开发组之外的语言进行更改。
你可以在 PEP 索引中查看 CPython 未来版本的起草的、拒绝的和接受的 PEP。
一旦 PEP 达成共识,并且草案已定稿,指导委员会必须接受或拒绝它。
PEP 13 中定义的指导委员会的任务规定,
他们应努力“维护 Python 语言和 CPython 解释器的质量和稳定性”。
The text was updated successfully, but these errors were encountered: