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中提供了此函数的定义。
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
函数的定义,并且紧随其后的是对该函数的深入说明。
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;
}
该函数接受三个值,两个
PyObject *
:v
和w
和一个整数值op_slot
,这是该操作在PyNumberMethods
结构中的偏移量。第5行和第6行定义了两个值
slotv
和slotw
,它们是表示其类型所建议的二进制函数的结构。从第5行到第15行,我们尝试取消引用
op_slot
对于给定参数v
和w
的函数。 在第10行,检查两个值是否具有相同的类型,如果两个值具有相同的类型,则无需在op_slot
中取消引用第二个值的函数。 即使这两个值不是同一类型,但从这两个函数取消引用的函数是相等的,则slotw
值将被清空。取消引用二进制函数后,如果
slotv
不为NULL
,则在第17行中,我们检查slotw
不为NULL
且w
的类型是v
的子类型,如果结果为true
,则将slotw
函数应用于v
和w
。 发生这种情况的原因是,如果您暂停思考一秒钟,那么继承树下的方法就是我们不想再使用的方法。 如果w不是子类型,则在第22行将slotv应用于这两个值。到达第29行意味着
slotv
函数为NULL
,因此只要不为NULL
,我们就对v
和w
应用任何slotw
引用。如果
slotv
和slotw
都不包含函数,则返回Py_NotImplemented
。Py_RETURN_NOTIMPLEMENTED
只是一个宏,它在返回Py_NotImplemented
值之前增加其引用计数。
上面给出的解释阐述了虚拟机是如何对提供给它的值执行操作的。我们在这里通过忽略可以重载的操作码来简化一些操作,例如 +
符号映射到 BINARY_ADD
操作码,并且可以应用于字符串,数字或序列。但是在上面的示例中,我们仅查看了适用于数字和数字子类的情况,很难想象如何处理重载操作。对于 BINARY_ADD
,如果人们查看 PyNumber_Add
函数,则可以看到,如果从 binary_op1
调用返回的值是 Py_NotImplemented
,则虚拟机将尝试将这些值视为序列,并尝试取消引用序列连接方法,然后将它们应用于两个输入值(如果它们实现了序列协议)。回到 ceval.c
中的解释器循环,当我们观察到对 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
Was this helpful?