# 7.5 一个示例：添加一个约等于比较运算法

如果要将本章节所有内容串联起来，你可以在 Python 语言中添加一个新的语法特性并重新编译 CPython 来掌握本章节内容。

**比较表达式**会比较两个或多个值：

<figure><img src="https://1029588898-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FJhewUmzI3BNeGgeFH9Rv%2Fuploads%2Fgit-blob-3adc0c969f42a754c46bd56a131a17648331125d%2F%E5%9B%BE7.5.1%20%E6%AF%94%E8%BE%83%E8%A1%A8%E8%BE%BE%E5%BC%8F.png?alt=media" alt=""><figcaption></figcaption></figure>

在比较表达式中用到的运算符被称为**比较运算符**。以下可能是一些你能认出来的比较运算符：

* 小于：<；
* 大于：>；
* 等于：==；
* 不等于：!=。

{% hint style="info" %}
**参见**

[PEP 207](https://peps.python.org/pep-0207/) 为 Python 2.1 提出了在数据模型中的丰富比较运算符。此 PEP 包含了自定义 Python 类型实现比较方法的上下文、历史以及解释。
{% endhint %}

现在，让我们来添加另外一个比较运算符：**约等于**，由 `~=` 表示，其将拥有如下行为：

* 如果你比较一个浮点数和一个整数，那么它会将浮点数转换为整数并比较结果；
* 如果你比较两个整数，那么它会使用普通的相等运算符。

新的运算符会在交互式解释器中返回如下结果：

<figure><img src="https://1029588898-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FJhewUmzI3BNeGgeFH9Rv%2Fuploads%2Fgit-blob-bf9edfd04aaf5ba1d66a1e1aa712c8cebdeea66c%2F%E5%9B%BE7.5.2%20%E7%BA%A6%E7%AD%89%E4%BA%8E%E8%A1%A8%E8%BE%BE%E5%BC%8F.png?alt=media" alt=""><figcaption></figcaption></figure>

为了添加新的运算符，首先你需要更新 CPython 语法。在 `Grammar/python.gram` 文件中，比较运算符被定义为一个符号（symbol）：`comp_op` ：

代码不翻译

修改 `compare_op_bitwise_or_pair` 表达式以允许新的 `ale_bitwise_or` 对：

代码不翻译

在现有的 `is_bitwise_or` 表达式下定义新的 `ale_bitwise_or` 表达式：

```c
...
is_bitwise_or[CmpopExprPair*]: 'is' a=bitwise_or ...
ale_bitwise_or[CmpopExprPair*]: '~=' a=bitwise_or
    { _PyPegen_cmpop_expr_pair(p, AlE, a) }
```

这个新的类型定义了一个包含 '\~=' 终结符号的命名表达式：`ale_bitwise_or`。

函数调用 `_PyPegen_cmpop_expr_pair(p, AlE, a)` 是一个从 AST 获取 `cmpop` 节点的表达式。类型为 `AlE`：**Al**most **E**qual。

接下来，在 `Grammar/Tokens` 中添加单词符号（token）：

```c
ATEQUAL '@='
RARROW '->'
ELLIPSIS '...'
COLONEQUAL ':='
# Add this line
ALMOSTEQUAL '~='
```

为了更新 C 语言中的语法和单词符号，你需要重新生成头文件。

在 MacOS 或 Linux上使用以下命令：

```bash
$ make regen-token regen-pegen
```

在 Windows 上运行 `PCBuild` 目录中的以下命令：

```bash
> build.bat --regen
```

这些步骤会自动更新分词器（tokenizer）。比如：打开 `Parser/token.c` 源文件，你可以看到 [PyToken\_TwoChars()](https://github.com/python/cpython/blob/v3.9.0b1/Parser/token.c#L109) 函数已经被修改：

<figure><img src="https://1029588898-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FJhewUmzI3BNeGgeFH9Rv%2Fuploads%2Fgit-blob-2963108ec8d243d238ba413eeaf2a4d83cb08ec1%2F%E5%9B%BE7.5.7%20%E6%9B%B4%E6%96%B0PyToken_TwoChars().png?alt=media" alt=""><figcaption></figcaption></figure>

如果你在这个阶段重新编译 CPython 并且打开一个交互式解释器，那么你可以看到分词器已经可以成功识别到这个单词符号，但是此时 AST 还不知道如何处理它：

<figure><img src="https://1029588898-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FJhewUmzI3BNeGgeFH9Rv%2Fuploads%2Fgit-blob-32851e697c559feb247ff915472c153fda7787f3%2F%E5%9B%BE7.5.8%20%E7%B3%BB%E7%BB%9F%E9%94%99%E8%AF%AF.png?alt=media" alt=""><figcaption></figcaption></figure>

这个异常是由 `Python/ast.c` 文件中的 [ast\_for\_comp\_op()](https://github.com/python/cpython/blob/v3.9.0b1/Python/ast.c#L1199) 函数引起，原因是交互式解释器还无法把 `ALMOSTEQUAL` 识别为一个比较运算符中的一个有效运算符。

`Compare` 是一个定义在 `Parser/Python.asdl` 文件中的一个表达式类型，其具有的属性：左表达式、操作符（`ops`）列表以及一个要比较的表达式列表（`comparators`）：

<figure><img src="https://1029588898-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FJhewUmzI3BNeGgeFH9Rv%2Fuploads%2Fgit-blob-8777ab2dc93519b4f4377d1c13ce6e76457cfa9c%2F%E5%9B%BE7.5.9%20asdl%E4%B8%ADCompare%E5%AE%9A%E4%B9%89.png?alt=media" alt=""><figcaption></figcaption></figure>

在 `Compare` 定义中是对 `cmpop` 枚举的引用：

<figure><img src="https://1029588898-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FJhewUmzI3BNeGgeFH9Rv%2Fuploads%2Fgit-blob-bca8e6f1ffa8fe91e687df6ea099961f25d66ab5%2F%E5%9B%BE7.5.10%20cmpop%E5%AE%9A%E4%B9%89.png?alt=media" alt=""><figcaption></figcaption></figure>

这是一个可以充当比较运算符的抽象语法树叶子节点的列表集合。我们定义的约等于运算符没在里面所以需要补充完整。更新这个选项集合来包含一个新类型：`ALE`：

<figure><img src="https://1029588898-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FJhewUmzI3BNeGgeFH9Rv%2Fuploads%2Fgit-blob-513db1a1146261ff6d6afd16e456cd391fbda1e5%2F%E5%9B%BE7.5.11%20%E4%BF%AE%E6%94%B9cmpop%E5%AE%9A%E4%B9%89.png?alt=media" alt=""><figcaption></figcaption></figure>

接下来，我们重新生成 AST 来更新 AST 的 C 头文件：

<figure><img src="https://1029588898-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FJhewUmzI3BNeGgeFH9Rv%2Fuploads%2Fgit-blob-9898876d65518b40329e7d4967b8976f60495b04%2F%E5%9B%BE7.5.12%20%E9%87%8D%E6%96%B0%E7%94%9F%E6%88%90%E6%8A%BD%E8%B1%A1%E8%AF%AD%E6%B3%95%E6%A0%91%E7%BB%93%E6%9E%84.png?alt=media" alt=""><figcaption></figcaption></figure>

这个操作会更新 `Include/Python-ast.h` 文件中的比较运算符（`_cmpop`）枚举，来包含 `ALE` 选项：

<figure><img src="https://1029588898-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FJhewUmzI3BNeGgeFH9Rv%2Fuploads%2Fgit-blob-ab55c8092f2b021c4cecbab033624f298c028708%2F%E5%9B%BE7.5.13%20Python-ast.h%E4%B8%AD%E9%87%8D%E6%96%B0%E7%94%9F%E6%88%90%E7%9A%84_cmpop.png?alt=media" alt=""><figcaption></figcaption></figure>

抽象语法树此时还没有能力将 `ALMOSTEQUAL` 单词符号和 `ALE` 比较运算符关联上。所以你还要为抽象语法树更新 C 语言代码。

找到 `Python/ast.c` 文件中的 [ast\_for\_comp\_op()](https://github.com/python/cpython/blob/v3.9.0b1/Python/ast.c#L1199) 函数并找到运算符单词符号的 switch 语句。这个函数会返回 `_cmop` 枚举值中的一个。

<figure><img src="https://1029588898-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FJhewUmzI3BNeGgeFH9Rv%2Fuploads%2Fgit-blob-d886db54ff33b68f40415b8d6f4b872a63c0474a%2F%E5%9B%BE7.5.14%20ast_for_comp_op%E5%AE%9A%E4%B9%891.png?alt=media" alt=""><figcaption></figcaption></figure>

添加两行代码，用于捕捉 `ALMOSTEQUAL` 单词符号并返回 `AlE` 比较运算符：

<figure><img src="https://1029588898-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FJhewUmzI3BNeGgeFH9Rv%2Fuploads%2Fgit-blob-6d0d098cf735e9752dd6b5dc0b2fdf22a109719e%2F%E5%9B%BE7.5.15%20ast_for_comp_op%E5%AE%9A%E4%B9%892.png?alt=media" alt=""><figcaption></figcaption></figure>

在这个阶段，分词器和 AST 可以解析代码，但是编译器还是不知道如何处理这个运算符。为了测试抽象语法树的表示，我们可以使用 `ast.parse()` 函数来探索表达式的第一个运算符：

<figure><img src="https://1029588898-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FJhewUmzI3BNeGgeFH9Rv%2Fuploads%2Fgit-blob-d1a5ade6abb4eb4dfcb2cdb8793e8bda7bce1cef%2F%E5%9B%BE7.5.17%20%E7%94%A8ast%E6%A8%A1%E5%9D%97%E8%A7%A3%E6%9E%90%E7%BA%A6%E7%AD%89%E4%BA%8E.png?alt=media" alt=""><figcaption></figcaption></figure>

这是我们 `AlE` 比较运算符类型的一个实例，因此抽象语法树才能正确的分析代码。

在下一个章节，你会继续学习 CPython 编译器是如何工作的，并继续修改约等于运算符来构建其执行逻辑。
