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
函数的第2行,对 binary_op1
函数的调用。 binary_op1
函数是另一个通用函数,该函数在其参数中包含两个值为数字或数字子类的值,并将一个二进制函数应用于这两个值。 NB_SLOT
宏将给定方法的偏移量返回到 PyNumberMethods
结构中。 回想一下,该结构是对数字起作用的方法的集合。 清单8.2中包含此类 binary_op1
函数的定义,并且紧随其后的是对该函数的深入说明。
该函数接受三个值,两个
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
操作码进行求值的情况时,我们会看到以下代码段:
在讨论解释器循环时,请忽略第1行和第2行。 从其余片段中我们看到的是,当我们遇到 BINARY_ADD
时,调用的第一个端口是检查两个值都是字符串,以便将字符串连接应用于这些值。 如果不是字符串,则将 Objects/abstract.c
中的 PyNumber_Add
函数应用于这两个值。 尽管代码在 Python/ceval.c
中完成的字符串检查以及在 Objects/abstract.c
中完成的数字和序列检查似乎有些混乱,但是当我们有一个重载的操作码时会发生什么是很明显的。
上面提供的解释是大多数操作码操作的处理方式,检查要计算的值的类型,然后根据需要取消引用该方法并将其应用于参数值。
Last updated
Was this helpful?