> For the complete documentation index, see [llms.txt](https://nanguage.gitbook.io/inside-python-vm-cn/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://nanguage.gitbook.io/inside-python-vm-cn/8.-intermezzo-the-abstract.c-module.md).

# 8. Intermezzo: The abstract.c Module

小插曲：abstract.c 模块

到目前为止，我们已经多次提到 python 虚拟机通常将要计算的值视为 `PyObject`。 这就留下了一个明显的问题：如何在此类通用对象上安全地执行操作？ 例如，当计算字节码指令 `BINARY_ADD` 时，会从求值堆栈中弹出两个 `PyObject` 值，并将其用作加法运算的参数，但是虚拟机如何知道这些值是否实际实现了加法运算所属的协议？

要了解 PyObject 上的许多操作如何工作，我们只需要查看 [`Objects/abstract.c`](https://github.com/python/cpython/blob/3.7/Objects/abstract.c) 模块。 该模块定义了许多对实现给定对象协议的对象起作用的函数。 这意味着，例如，要将两个对象相加，则此模块中的 `add` 函数将期望两个对象都实现了 `tp_numbers` 字段的 `__add__` 方法。 解释此问题的最佳方法是举例说明。

考虑 BINARY\_ADD 操作码的情况，当将它应用于两个数字的加法运算时，将调用 `Objects/abstract.c` 模块的 [`PyNumber_Add`](https://github.com/python/cpython/blob/3.7/Objects/abstract.c#L955) 函数。 清单8.1中提供了此函数的定义。

{% code title="代码清单8.1: PyNumber\_Add 函数" %}

```c
PyObject *
PyNumber_Add(PyObject *v, PyObject *w)
{
    PyObject *result = binary_op1(v, w, NB_SLOT(nb_add));
    if (result == Py_NotImplemented) {
        PySequenceMethods *m = v->ob_type->tp_as_sequence;
        Py_DECREF(result);
        if (m && m->sq_concat) {
            return (*m->sq_concat)(v, w);
        }
        result = binop_type_error(v, w, "+");
    }
    return result;
}
```

{% endcode %}

这时，我们感兴趣的是清单8.1中 `PyNumber_Add` 函数的第2行，对 [`binary_op1`](https://github.com/python/cpython/blob/3.7/Objects/abstract.c#L785) 函数的调用。 `binary_op1` 函数是另一个通用函数，该函数在其参数中包含两个值为数字或数字子类的值，并将一个二进制函数应用于这两个值。 [`NB_SLOT`](https://github.com/python/cpython/blob/3.7/Objects/abstract.c#L768)  宏将给定方法的偏移量返回到 [`PyNumberMethods`](https://github.com/python/cpython/blob/f95cd199b4bc16775c8c48641bd85416b17742e7/Include/cpython/object.h#L111) 结构中。 回想一下，该结构是对数字起作用的方法的集合。 清单8.2中包含此类 `binary_op1` 函数的定义，并且紧随其后的是对该函数的深入说明。

{% code title="代码清单8.2: binary\_op1 函数" %}

```c
static PyObject *
binary_op1(PyObject *v, PyObject *w, const int op_slot)
{
    PyObject *x;
    binaryfunc slotv = NULL;
    binaryfunc slotw = NULL;

    if (v->ob_type->tp_as_number != NULL)
        slotv = NB_BINOP(v->ob_type->tp_as_number, op_slot);
    if (w->ob_type != v->ob_type &&
        w->ob_type->tp_as_number != NULL) {
        slotw = NB_BINOP(w->ob_type->tp_as_number, op_slot);
        if (slotw == slotv)
            slotw = NULL;
    }
    if (slotv) {
        if (slotw && PyType_IsSubtype(w->ob_type, v->ob_type)) {
            x = slotw(v, w);
            if (x != Py_NotImplemented)
                return x;
            Py_DECREF(x); /* can't do it */
            slotw = NULL;
        }
        x = slotv(v, w);
        if (x != Py_NotImplemented)
            return x;
        Py_DECREF(x); /* can't do it */
    }
    if (slotw) {
        x = slotw(v, w);
        if (x != Py_NotImplemented)
            return x;
        Py_DECREF(x); /* can't do it */
    }
    Py_RETURN_NOTIMPLEMENTED;
}

```

{% endcode %}

1. 该函数接受三个值，两个 `PyObject *`：`v` 和 `w` 和一个整数值 `op_slot`，这是该操作在 `PyNumberMethods` 结构中的偏移量。
2. 第5行和第6行定义了两个值 `slotv` 和 `slotw`，它们是表示其类型所建议的二进制函数的结构。
3. 从第5行到第15行，我们尝试取消引用 `op_slot` 对于给定参数 `v` 和 `w` 的函数。 在第10行，检查两个值是否具有相同的类型，如果两个值具有相同的类型，则无需在 `op_slot` 中取消引用第二个值的函数。 即使这两个值不是同一类型，但从这两个函数取消引用的函数是相等的，则 `slotw` 值将被清空。
4. 取消引用二进制函数后，如果 `slotv` 不为 `NULL`，则在第17行中，我们检查 `slotw` 不为 `NULL` 且 `w` 的类型是 `v` 的子类型，如果结果为 `true`，则将 `slotw` 函数应用于 `v` 和 `w`。 发生这种情况的原因是，如果您暂停思考一秒钟，那么继承树下的方法就是我们不想再使用的方法。 如果w不是子类型，则在第22行将slotv应用于这两个值。
5. 到达第29行意味着 `slotv` 函数为 `NULL`，因此只要不为 `NULL`，我们就对 `v` 和 `w` 应用任何 `slotw` 引用。
6. 如果 `slotv` 和 `slotw` 都不包含函数，则返回 `Py_NotImplemented`。 `Py_RETURN_NOTIMPLEMENTED` 只是一个宏，它在返回 `Py_NotImplemented` 值之前增加其引用计数。

上面给出的解释阐述了虚拟机是如何对提供给它的值执行操作的。我们在这里通过忽略可以重载的操作码来简化一些操作，例如 `+` 符号映射到 `BINARY_ADD` 操作码，并且可以应用于字符串，数字或序列。但是在上面的示例中，我们仅查看了适用于数字和数字子类的情况，很难想象如何处理重载操作。对于 `BINARY_ADD`，如果人们查看 `PyNumber_Add` 函数，则可以看到，如果从 `binary_op1` 调用返回的值是 `Py_NotImplemented`，则虚拟机将尝试将这些值视为序列，并尝试取消引用序列连接方法，然后将它们应用于两个输入值（如果它们实现了序列协议）。回到 `ceval.c` 中的解释器循环，当我们观察到对 `BINARY_ADD` 操作码进行求值的情况时，我们会看到以下代码段：

{% code title="代码清单8.3: ceval 中 binary add 的实现" %}

```c
            PyObject *right = POP();
            PyObject *left = TOP();
            PyObject *sum;
            if (PyUnicode_CheckExact(left) &&
                     PyUnicode_CheckExact(right)) {
                sum = unicode_concatenate(left, right, f, next_instr);
                /* unicode_concatenate consumed the ref to left */
            }
            else {
                sum = PyNumber_Add(left, right);
                Py_DECREF(left);
            }
```

{% endcode %}

在讨论解释器循环时，请忽略第1行和第2行。 从其余片段中我们看到的是，当我们遇到 `BINARY_ADD` 时，调用的第一个端口是检查两个值都是字符串，以便将字符串连接应用于这些值。 如果不是字符串，则将 `Objects/abstract.c` 中的 `PyNumber_Add` 函数应用于这两个值。 尽管代码在 `Python/ceval.c` 中完成的字符串检查以及在 `Objects/abstract.c` 中完成的数字和序列检查似乎有些混乱，但是当我们有一个重载的操作码时会发生什么是很明显的。

上面提供的解释是大多数操作码操作的处理方式，检查要计算的值的类型，然后根据需要取消引用该方法并将其应用于参数值。
