8. Intermezzo: The abstract.c Module

小插曲:abstract.c 模块

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

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

考虑 BINARY_ADD 操作码的情况,当将它应用于两个数字的加法运算时,将调用 Objects/abstract.c 模块的 PyNumber_Add 函数。 清单8.1中提供了此函数的定义。

代码清单8.1: PyNumber_Add 函数
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;
}

这时,我们感兴趣的是清单8.1中 PyNumber_Add 函数的第2行,对 binary_op1 函数的调用。 binary_op1 函数是另一个通用函数,该函数在其参数中包含两个值为数字或数字子类的值,并将一个二进制函数应用于这两个值。 NB_SLOT 宏将给定方法的偏移量返回到 PyNumberMethods 结构中。 回想一下,该结构是对数字起作用的方法的集合。 清单8.2中包含此类 binary_op1 函数的定义,并且紧随其后的是对该函数的深入说明。

代码清单8.2: binary_op1 函数
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;
}
  1. 该函数接受三个值,两个 PyObject *vw 和一个整数值 op_slot,这是该操作在 PyNumberMethods 结构中的偏移量。

  2. 第5行和第6行定义了两个值 slotvslotw,它们是表示其类型所建议的二进制函数的结构。

  3. 从第5行到第15行,我们尝试取消引用 op_slot 对于给定参数 vw 的函数。 在第10行,检查两个值是否具有相同的类型,如果两个值具有相同的类型,则无需在 op_slot 中取消引用第二个值的函数。 即使这两个值不是同一类型,但从这两个函数取消引用的函数是相等的,则 slotw 值将被清空。

  4. 取消引用二进制函数后,如果 slotv 不为 NULL,则在第17行中,我们检查 slotw 不为 NULLw 的类型是 v 的子类型,如果结果为 true,则将 slotw 函数应用于 vw。 发生这种情况的原因是,如果您暂停思考一秒钟,那么继承树下的方法就是我们不想再使用的方法。 如果w不是子类型,则在第22行将slotv应用于这两个值。

  5. 到达第29行意味着 slotv 函数为 NULL,因此只要不为 NULL,我们就对 vw 应用任何 slotw 引用。

  6. 如果 slotvslotw 都不包含函数,则返回 Py_NotImplementedPy_RETURN_NOTIMPLEMENTED 只是一个宏,它在返回 Py_NotImplemented 值之前增加其引用计数。

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

代码清单8.3: ceval 中 binary add 的实现
            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);
            }

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

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

Last updated