# 8. Intermezzo: The abstract.c Module

到目前为止，我们已经多次提到 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` 中完成的数字和序列检查似乎有些混乱，但是当我们有一个重载的操作码时会发生什么是很明显的。

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://nanguage.gitbook.io/inside-python-vm-cn/8.-intermezzo-the-abstract.c-module.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
