Python对象系统的工作原理¶
原文链接¶
https://tenthousandmeters.com/blog/python-behind-the-scenes-6-how-python-object-system-works/
正如我们从本系列前面的部分中所知,Python程序的执行包括两个主要步骤:
- CPython编译器将Python代码转换为字节码。
- CPython VM执行字节码。
我们关注第二步已经有一段时间了。在第4部分中,我们研究了求值循环,Python字节码在其中执行。在第5部分中,我们研究了VM如何执行用于实现变量的指令。
我们还没有讨论的是VM实际上是如何计算的。我们推迟了这个问题,因为要回答这个问题,我们首先需要了解语言最基本的部分是如何工作的。今天,我们将研究Python对象系统。
注:在本文中,我指的是CPython 3.9。随着CPython的发展,一些实现细节肯定会发生变化。我将努力跟踪重要的更改并添加更新注释。
动机¶
考虑一段极其简单的Python代码:
def f(x):
return x + 7
- 它调用x.add(7)或type(x)add(x,7)
- 如果x没有__add__(),或者如果此方法失败,则调用(7)__radd_(x)或int__radd_(7, x)
然而,现实情况有点复杂。真正发生的事情取决于x是什么。例如,如果x是用户定义类的实例,上面描述的算法类似于事实。然而,如果x是内置类型的实例, 比如int或float,CPython根本不会调用任何特殊方法。 要了解一些Python代码是如何执行的,我们可以执行以下操作:
- 将代码分解为字节码。
- 研究VM如何执行反汇编的字节码指令。
让我们将此算法应用于函数f。编译器将此函数的主体转换为以下字节码:
$ python -m dis f.py
...
2 0 LOAD_FAST 0 (x)
2 LOAD_CONST 1 (7)
4 BINARY_ADD
6 RETURN_VALUE
VM如何添加两个值?要回答这个问题,我们需要了解这些值是什么。对我们来说,7是int的一个实例,x是任何东西。不过,对于VM来说,一切都是Python对象。 VM推送到堆栈上并从堆栈中弹出的所有值都是指向PyObject结构的指针(因此,短语“Python中的一切都是对象”)。VM不需要知道如何添加整数或字符串,也就是说, 如何进行算术或串联序列。它只需要知道每个Python对象都有一个类型。一个类型反过来知道它的对象的一切。例如,int类型知道如何添加整数,float类型知道如何增加浮点。因此,VM请求类型执行操作。 这种简化的解释抓住了解决方案的本质,但也省略了许多重要的细节。为了获得更真实的画面,我们需要了解Python对象和类型的真正含义以及它们的工作方式。
Python对象和类型¶
我们在第3部分中讨论了Python对象。这个讨论值得在这里重复。我们从PyObject结构的定义开始:
typedef struct _object {
_PyObject_HEAD_EXTRA // macro, for debugging purposes only
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
} PyObject;
- CPython用于垃圾收集的引用计数ob_refcnt;和
- 指向对象类型ob_type的指针。
我们说过VM将任何Python对象视为PyObject。这怎么可能?C编程语言没有类和继承的概念。然而,在C语言中实现一些可以称为单个继承的东西是可能的。C标准规定, 指向任何结构的指针都可以转换为指向其第一个成员的指针,反之亦然。因此,我们可以通过定义第一个成员为PyObject的新结构来“扩展”PyObject。 例如,以下是如何定义float对象的:
typedef struct {
PyObject ob_base; // expansion of PyObject_HEAD macro
double ob_fval;
} PyFloatObject;
PyFloatObject float_object;
// ...
PyObject *obj_ptr = (PyObject *)&float_object;
PyFloatObject *float_obj_ptr = (PyFloatObject *)obj_ptr;
// PyTypeObject is a typedef for "struct _typeobject"
struct _typeobject {
PyVarObject ob_base; // expansion of PyObject_VAR_HEAD macro
const char *tp_name; /* For printing, in format "<module>.<name>" */
Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */
/* Methods to implement standard operations */
destructor tp_dealloc;
Py_ssize_t tp_vectorcall_offset;
getattrfunc tp_getattr;
setattrfunc tp_setattr;
PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
or tp_reserved (Python 3) */
reprfunc tp_repr;
/* Method suites for standard classes */
PyNumberMethods *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods *tp_as_mapping;
/* More standard operations (here for binary compatibility) */
hashfunc tp_hash;
ternaryfunc tp_call;
reprfunc tp_str;
getattrofunc tp_getattro;
setattrofunc tp_setattro;
/* Functions to access object as input/output buffer */
PyBufferProcs *tp_as_buffer;
/* Flags to define presence of optional/expanded features */
unsigned long tp_flags;
const char *tp_doc; /* Documentation string */
/* Assigned meaning in release 2.0 */
/* call function for all accessible objects */
traverseproc tp_traverse;
/* delete references to contained objects */
inquiry tp_clear;
/* Assigned meaning in release 2.1 */
/* rich comparisons */
richcmpfunc tp_richcompare;
/* weak reference enabler */
Py_ssize_t tp_weaklistoffset;
/* Iterators */
getiterfunc tp_iter;
iternextfunc tp_iternext;
/* Attribute descriptor and subclassing stuff */
struct PyMethodDef *tp_methods;
struct PyMemberDef *tp_members;
struct PyGetSetDef *tp_getset;
struct _typeobject *tp_base;
PyObject *tp_dict;
descrgetfunc tp_descr_get;
descrsetfunc tp_descr_set;
Py_ssize_t tp_dictoffset;
initproc tp_init;
allocfunc tp_alloc;
newfunc tp_new;
freefunc tp_free; /* Low-level free-memory routine */
inquiry tp_is_gc; /* For PyObject_IS_GC */
PyObject *tp_bases;
PyObject *tp_mro; /* method resolution order */
PyObject *tp_cache;
PyObject *tp_subclasses;
PyObject *tp_weaklist;
destructor tp_del;
/* Type attribute cache version tag. Added in version 2.6 */
unsigned int tp_version_tag;
destructor tp_finalize;
vectorcallfunc tp_vectorcall;
};
typedef struct {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
} PyVarObject;
- tp_new是指向创建该类型新对象的函数的指针。
- tp_str是指向一个函数的指针,该函数为该类型的对象实现str()。
- tp_hash是指向一个函数的指针,该函数为该类型的对象实现hash()。
一些插槽(称为子插槽)在套件中组合在一起。套件只是包含相关槽的结构。例如,PySequenceMethods结构是一套实现序列协议的子槽:
typedef struct {
lenfunc sq_length;
binaryfunc sq_concat;
ssizeargfunc sq_repeat;
ssizeargfunc sq_item;
void *was_sq_slice;
ssizeobjargproc sq_ass_item;
void *was_sq_ass_slice;
objobjproc sq_contains;
binaryfunc sq_inplace_concat;
ssizeargfunc sq_inplace_repeat;
} PySequenceMethods;
typedef struct {
binaryfunc nb_add; // typedef PyObject * (*binaryfunc)(PyObject *, PyObject *)
binaryfunc nb_subtract;
binaryfunc nb_multiply;
binaryfunc nb_remainder;
binaryfunc nb_divmod;
// ... more sub-slots
} PyNumberMethods;
- 设置为什么?
- 它是如何使用的?
我认为最好从第二个开始。我们应该期望VM调用nb_add来执行BINARY_add操作码。因此,让我们暂时暂停对类型的讨论,看看BINARY_ADD操作码是如何实现的。
BINARY_ADD¶
与任何其他操作码一样,BINARY_ADD在Python/ceval.c中的求值循环中实现:
case TARGET(BINARY_ADD): {
PyObject *right = POP();
PyObject *left = TOP();
PyObject *sum;
/* NOTE(haypo): Please don't try to micro-optimize int+int on
CPython using bytecode, it is simply worthless.
See http://bugs.python.org/issue21955 and
http://bugs.python.org/issue10044 for the discussion. In short,
no patch shown any impact on a realistic benchmark, only a minor
speedup on microbenchmarks. */
if (PyUnicode_CheckExact(left) &&
PyUnicode_CheckExact(right)) {
sum = unicode_concatenate(tstate, left, right, f, next_instr);
/* unicode_concatenate consumed the ref to left */
}
else {
sum = PyNumber_Add(left, right);
Py_DECREF(left);
}
Py_DECREF(right);
SET_TOP(sum);
if (sum == NULL)
goto error;
DISPATCH();
}
output += some_string
PyObject *
PyNumber_Add(PyObject *v, PyObject *w)
{
// NB_SLOT(nb_add) expands to "offsetof(PyNumberMethods, nb_add)"
PyObject *result = binary_op1(v, w, NB_SLOT(nb_add));
if (result == Py_NotImplemented) {
PySequenceMethods *m = Py_TYPE(v)->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;
}
static PyObject *
binary_op1(PyObject *v, PyObject *w, const int op_slot)
{
PyObject *x;
binaryfunc slotv = NULL;
binaryfunc slotw = NULL;
if (Py_TYPE(v)->tp_as_number != NULL)
slotv = NB_BINOP(Py_TYPE(v)->tp_as_number, op_slot);
if (!Py_IS_TYPE(w, Py_TYPE(v)) &&
Py_TYPE(w)->tp_as_number != NULL) {
slotw = NB_BINOP(Py_TYPE(w)->tp_as_number, op_slot);
if (slotw == slotv)
slotw = NULL;
}
if (slotv) {
if (slotw && PyType_IsSubtype(Py_TYPE(w), Py_TYPE(v))) {
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;
}
- 如果一个操作数的类型是另一操作数的子类型,请调用该子类型的槽。
- 如果左操作数没有槽,则调用右操作数的槽。
- 否则,调用左操作数的槽。
为子类型的槽设置优先级的原因是允许子类型覆盖其祖先的行为:
$ python -q
>>> class HungryInt(int):
... def __add__(self, o):
... return self
...
>>> x = HungryInt(5)
>>> x + 2
5
>>> 2 + x
7
>>> HungryInt.__radd__ = lambda self, o: self
>>> 2 + x
5
PySequenceMethods *m = Py_TYPE(v)->tp_as_sequence;
if (m && m->sq_concat) {
return (*m->sq_concat)(v, w);
}
- nbadd表示具有财产的代数加法,如a+b=b+a。
- sq_concat表示序列的串联。
内置类型如int和float实现nb_add,以及内置类型如str和list实现sq_concat。从技术上讲,没有太大区别。选择一个槽而不是另一个槽的主要原因是为了表明适当的含义。 事实上,sq_concat槽是如此不必要,以至于对于所有用户定义的类型(即类),它都设置为NULL。 我们看到了nb_add槽的用法:它由binary_op1()函数调用。下一步是查看它的设置。
nb_add可以是什么¶
由于加法对于不同的类型是不同的操作,因此类型的nb_add槽必须是以下两项之一:
- 它要么是添加该类型对象的特定类型函数;或
- 它是一个类型不可知的函数,它调用一些特定于类型的函数,例如类型的__add__()特殊方法。
它确实是这两种中的一种,哪一种取决于类型。例如,内置类型(如int和float)有自己的nb_add实现。相反,所有类共享相同的实现。从根本上讲,内置类型和类是同一回事——PyTypeObject的实例。 它们之间的重要区别在于它们是如何创建的。这种差异会影响插槽的设置方式,因此我们应该对此进行讨论。
创建类型的方法¶
创建类型对象有两种方法:
- 通过静态定义它;或
- 通过动态分配它。
静态定义的类型¶
静态定义类型的一个示例是任何内置类型。例如,以下是CPython如何定义float类型:
PyTypeObject PyFloat_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"float",
sizeof(PyFloatObject),
0,
(destructor)float_dealloc, /* tp_dealloc */
0, /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
(reprfunc)float_repr, /* tp_repr */
&float_as_number, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
(hashfunc)float_hash, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
float_new__doc__, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
float_richcompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
float_methods, /* tp_methods */
0, /* tp_members */
float_getset, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
float_new, /* tp_new */
};
static PyNumberMethods float_as_number = {
float_add, /* nb_add */
float_sub, /* nb_subtract */
float_mul, /* nb_multiply */
// ... more number slots
};
static PyObject *
float_add(PyObject *v, PyObject *w)
{
double a,b;
CONVERT_TO_DOUBLE(v, a);
CONVERT_TO_DOUBLE(w, b);
a = a + b;
return PyFloat_FromDouble(a);
}
动态分配的类型¶
动态分配的类型是我们使用class语句定义的类型。正如我们已经说过的,它们是PyTypeObject的实例,就像静态定义的类型一样。传统上,我们称它们为类,但也可以称它们为用户定义的类型。 从程序员的角度来看,在Python中定义类比在C中定义类型更容易。这是因为CPython在创建类时会在幕后做很多事情。让我们看看这个过程中涉及到什么。 如果我们不知道从哪里开始,我们可以采用熟悉的方法:
- 定义一个简单类:
class A: pass
- 运行拆装器:
$ python -m dis class_A.py
- 研究VM如何执行生成的字节码指令。
如果你有时间的话,可以随意做,或者阅读Eli Bendersky的课程文章。我们走捷径。 对象是通过调用类型创建的,例如list()或MyClass()。类是通过调用元类型创建的。元类型只是其实例为类型的类型。Python有一个名为PyType_Type的内置元类型, 我们简单地称之为类型。以下是它的定义:
PyTypeObject PyType_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"type", /* tp_name */
sizeof(PyHeapTypeObject), /* tp_basicsize */
sizeof(PyMemberDef), /* tp_itemsize */
(destructor)type_dealloc, /* tp_dealloc */
offsetof(PyTypeObject, tp_vectorcall), /* tp_vectorcall_offset */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_as_async */
(reprfunc)type_repr, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
(ternaryfunc)type_call, /* tp_call */
0, /* tp_str */
(getattrofunc)type_getattro, /* tp_getattro */
(setattrofunc)type_setattro, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
Py_TPFLAGS_BASETYPE | Py_TPFLAGS_TYPE_SUBCLASS |
Py_TPFLAGS_HAVE_VECTORCALL, /* tp_flags */
type_doc, /* tp_doc */
(traverseproc)type_traverse, /* tp_traverse */
(inquiry)type_clear, /* tp_clear */
0, /* tp_richcompare */
offsetof(PyTypeObject, tp_weaklist), /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
type_methods, /* tp_methods */
type_members, /* tp_members */
type_getsets, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
offsetof(PyTypeObject, tp_dict), /* tp_dictoffset */
type_init, /* tp_init */
0, /* tp_alloc */
type_new, /* tp_new */
PyObject_GC_Del, /* tp_free */
(inquiry)type_is_gc, /* tp_is_gc */
};
- 它调用类型的tp_new来创建对象。
- 它调用类型的tp_init来初始化创建的对象。
类型的类型是类型本身。因此,当我们调用type()时,会调用type_call()函数。当我们向type()传递一个参数时,它会检查特殊情况。在这种情况下,type_call()只返回传递对象的类型:
$ python -q
>>> type(3)
<class 'int'>
>>> type(int)
<class 'type'>
>>> type(type)
<class 'type'>
$ python -q
>>> MyClass = type('MyClass', (), {'__str__': lambda self: 'Hey!'})
>>> instance_of_my_class = MyClass()
>>> str(instance_of_my_class)
Hey!
- 类的名称
- 其基元组;和
- 命名空间。
其他元类型也采用这种形式的参数。 我们看到我们可以通过调用type()来创建类,但这不是我们通常所做的。通常,我们使用class语句来定义类。事实证明,在这种情况下,VM最终也会调用某个元类型,并且通常会调用type()。 为了执行class语句,VM从内置模块调用__build_class__()函数。此函数的作用可概括如下:
- 决定要调用哪个元类型来创建类。
- 准备命名空间。命名空间将用作类的字典。
- 在命名空间中执行类的主体,从而填充命名空间。
- 调用元类型。
我们可以使用元类关键字指示__build_class__()它应该调用哪个元类型。如果未指定元类,build_class()默认调用type()。它还考虑了碱基的元类型。 文档中很好地描述了选择元类型的确切逻辑。假设我们定义了一个新类,但没有指定元类。类实际上是在哪里创建的?在这种情况下,build_class()调用type()。 这将调用type_call()函数,该函数依次调用类型为的tp_new和tp_init插槽。类型的tp_new槽指向type_new()函数。这是创建类的函数。类型的tp_init槽指向什么都不做的函数, 因此所有的工作都由type_new()完成。 type_new()函数将近500行长,可能需要单独发布。然而,其本质可以概括如下:
- 分配新类型对象。
- 设置分配的类型对象。
要完成第一步,type_new()必须分配PyTypeObject的实例以及套件。套件必须与PyTypeObject分开分配,因为PyTypeObject只包含指向套件的指针,而不包含套件本身。 为了处理这种不便,type_new()分配了PyHeapTypeObject结构的一个实例,该结构扩展了PyTypeObject并包含以下套件:
/* The *real* layout of a type object when allocated on the heap */
typedef struct _heaptypeobject {
PyTypeObject ht_type;
PyAsyncMethods as_async;
PyNumberMethods as_number;
PyMappingMethods as_mapping;
PySequenceMethods as_sequence;
PyBufferProcs as_buffer;
PyObject *ht_name, *ht_slots, *ht_qualname;
struct _dictkeysobject *ht_cached_keys;
PyObject *ht_module;
/* here are optional user slots, followed by the members. */
} PyHeapTypeObject;
类型初始化¶
在可以使用任何类型之前,应该使用PyType_Ready()
函数对其进行初始化。对于类,PyType_Ready()
由type_new()
调用。对于静态定义的类型,
必须显式调用PyType_Ready()
。当 CPython 启动时,它会为每个内置类型调用PyType_Ready()
。 PyType_Ready()
函数做了很多事情。例如,它进行插槽继承。
插槽继承¶
当我们定义一个继承自其他类型的类时,我们希望该类继承该类型的某些行为。例如,当我们定义一个继承自 int 的类时,我们期望它支持加法:
$ python -q
>>> class MyInt(int):
... pass
...
>>> x = MyInt(2)
>>> y = MyInt(4)
>>> x + y
6
终极问题¶
到目前为止,我们已经看到了两种设置插槽的方法:
- 它可以明确指定(如果类型是静态定义的类型)。
- 它可以从祖先那里继承。
目前还不清楚一个类的槽是如何连接到它的特殊方法的。此外,对于内置类型,我们有一个逆向问题。他们如何实施特殊方法?他们当然会:
$ python -q
>>> (3).__add__(4)
7
特殊方法和槽¶
答案在于 CPython 保持特殊方法和槽之间的映射。该映射由 slotdefs 数组表示。它看起来像这样:
#define TPSLOT(NAME, SLOT, FUNCTION, WRAPPER, DOC) \
{NAME, offsetof(PyTypeObject, SLOT), (void *)(FUNCTION), WRAPPER, \
PyDoc_STR(DOC)}
static slotdef slotdefs[] = {
TPSLOT("__getattribute__", tp_getattr, NULL, NULL, ""),
TPSLOT("__getattr__", tp_getattr, NULL, NULL, ""),
TPSLOT("__setattr__", tp_setattr, NULL, NULL, ""),
TPSLOT("__delattr__", tp_setattr, NULL, NULL, ""),
TPSLOT("__repr__", tp_repr, slot_tp_repr, wrap_unaryfunc,
"__repr__($self, /)\n--\n\nReturn repr(self)."),
TPSLOT("__hash__", tp_hash, slot_tp_hash, wrap_hashfunc,
"__hash__($self, /)\n--\n\nReturn hash(self)."),
// ... more slotdefs
}
// typedef struct wrapperbase slotdef;
struct wrapperbase {
const char *name;
int offset;
void *function;
wrapperfunc wrapper;
const char *doc;
int flags;
PyObject *name_strobj;
};
- name 是一个特殊方法的名称。
- offset 是 PyHeapTypeObject 结构中槽的偏移量。它指定与特殊方法对应的插槽。
- function是插槽的实现。当定义了一个特殊的方法时,相应的槽被设置为函数。通常,函数调用特殊方法来完成工作。
- wrapper 是围绕插槽的包装函数。定义插槽时,包装器会为相应的特殊方法提供实现。它调用插槽来完成工作。
例如,这是一个将 add() 特殊方法映射到 nb_add 插槽的条目:
- 名称是“add”。
- 偏移量是 offsetof(PyHeapTypeObject, as_number.nb_add)。
- 函数是 slot_nb_add()。
- 包装器是 wrap_binaryfunc_l()。
slotdefs 数组是一个多对多映射。例如,正如我们将看到的, add() 和 radd() 特殊方法都映射到同一个 nb_add 槽。相反, mp_subscript“映射”插槽和 sq_item“序列”插槽都映射到相同的 getitem() 特殊方法。
CPython 以两种方式使用 slotdefs 数组:
- 根据特殊方法设置插槽;和
- 设置基于插槽的特殊方法。
基于特殊方法的插槽¶
type_new() 函数调用 fixup_slot_dispatchers() 以根据特殊方法设置插槽。 fixup_slot_dispatchers() 函数为 slotdefs 数组中的每个槽调用 update_one_slot() , 并且 update_one_slot() 将槽设置为在类具有相应的特殊方法时起作用。 我们以 nb_add 插槽为例。 slotdefs 数组有两个对应于该插槽的条目:
static slotdef slotdefs[] = {
// ...
BINSLOT("__add__", nb_add, slot_nb_add, "+"),
RBINSLOT("__radd__", nb_add, slot_nb_add,"+"),
// ...
}
static slotdef slotdefs[] = {
// ...
// {name, offset, function,
// wrapper, doc}
//
{"__add__", offsetof(PyHeapTypeObject, as_number.nb_add), (void *)(slot_nb_add),
wrap_binaryfunc_l, PyDoc_STR("__add__" "($self, value, /)\n--\n\nReturn self" "+" "value.")},
{"__radd__", offsetof(PyHeapTypeObject, as_number.nb_add), (void *)(slot_nb_add),
wrap_binaryfunc_r, PyDoc_STR("__radd__" "($self, value, /)\n--\n\nReturn value" "+" "self.")},
// ...
}
现在,你问什么是 slot_nb_add()?此函数使用扩展如下的宏定义:
static PyObject *
slot_nb_add(PyObject *self, PyObject *other) {
PyObject* stack[2];
PyThreadState *tstate = _PyThreadState_GET();
_Py_static_string(op_id, "__add__");
_Py_static_string(rop_id, "__radd__");
int do_other = !Py_IS_TYPE(self, Py_TYPE(other)) && \
Py_TYPE(other)->tp_as_number != NULL && \
Py_TYPE(other)->tp_as_number->nb_add == slot_nb_add;
if (Py_TYPE(self)->tp_as_number != NULL && \
Py_TYPE(self)->tp_as_number->nb_add == slot_nb_add) {
PyObject *r;
if (do_other && PyType_IsSubtype(Py_TYPE(other), Py_TYPE(self))) {
int ok = method_is_overloaded(self, other, &rop_id);
if (ok < 0) {
return NULL;
}
if (ok) {
stack[0] = other;
stack[1] = self;
r = vectorcall_maybe(tstate, &rop_id, stack, 2);
if (r != Py_NotImplemented)
return r;
Py_DECREF(r); do_other = 0;
}
}
stack[0] = self;
stack[1] = other;
r = vectorcall_maybe(tstate, &op_id, stack, 2);
if (r != Py_NotImplemented || Py_IS_TYPE(other, Py_TYPE(self)))
return r;
Py_DECREF(r);
}
if (do_other) {
stack[0] = other;
stack[1] = self;
return vectorcall_maybe(tstate, &rop_id, stack, 2);
}
Py_RETURN_NOTIMPLEMENTED;
}
在现有类上设置特殊方法¶
假设我们创建了一个没有 add() 和 radd() 特殊方法的类。在这种情况下,类的 nb_add 槽被设置为 NULL。正如预期的那样,我们无法添加该类的实例。然而,如果我们在创建类之后设置 add() 或 radd() ,那么添加就好像该方法是类定义的一部分一样。这就是我的意思:
$ python -q
>>> class A:
... pass
...
>>> x = A()
>>> x + 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'A' and 'int'
>>> A.__add__ = lambda self, o: 5
>>> x + 2
5
>>>
在我们了解 CPython 如何进行反向操作(即它如何向内置类型添加特殊方法)之前,我们需要了解什么是方法。
方法¶
方法是一种属性,但是是一种特殊的属性。当我们从实例调用方法时,该方法隐式接收实例作为其第一个参数,我们通常将其表示为 self:
$ python -q
>>> class A:
... def method(self, x):
... return self, x
...
>>> a = A()
>>> a.method(1)
(<__main__.A object at 0x10d10bfd0>, 1)
>>> A.method(a, 1)
(<__main__.A object at 0x10d10bfd0>, 1)
首先,要意识到我们在类上定义的方法只是一个函数。通过实例访问的函数不同于通过实例类型访问的相同函数,因为函数类型实现了描述符协议。如果您不熟悉描述符,我强烈建议您阅读 Raymond Hettinger 撰写的 Descriptor HowTo Guide。简而言之,描述符是一个对象,当用作属性时,它自己决定如何获取、设置和删除它。从技术上讲,描述符是一个实现了 get()、set() 或 delete() 特殊方法的对象。
函数类型实现了 get()。当我们查找某个方法时,我们得到的是调用 get() 的结果。传递给它的三个参数:
- 一个属性,即一个函数
- 一个实例
- 实例的类型。
如果我们查找某个类型的方法,实例为 NULL,而 get() 只是返回该函数。如果我们在一个实例上查找一个方法, get() 返回一个方法对象:
>>> type(A.method)
<class 'function'>
>>> type(a.method)
<class 'method'>
现在我们准备好解决最后一个问题。
基于槽的特殊方法¶
回想一下初始化类型和进行槽继承的 PyType_Ready() 函数。它还根据已实现的插槽向类型添加特殊方法。 PyType_Ready() 调用 add_operators() 来做到这一点。 add_operators() 函数迭代 slotdefs 数组中的条目。对于每个条目,它检查条目指定的特殊方法是否应该添加到类型的字典中。如果尚未定义特殊方法并且类型实现条目指定的槽,则添加特殊方法。例如,如果 add() 特殊方法未在类型上定义,但该类型实现了 nb_add 槽,则 add_operators() 将 add() 放入该类型的字典中。
add() 设置为什么?与任何其他方法一样,它必须设置为某个描述符才能表现得像一个方法。程序员定义的方法是函数,而 add_operators() 设置的方法是包装器描述符。包装器描述符是一个存储两件事的描述符:
- 它存储一个包装槽。包装槽“完成”特殊方法的工作。例如,float 类型的 add() 特殊方法的包装器描述符将 float_add() 存储为包装槽。
- 它存储一个包装函数。包装函数“知道”如何调用包装的插槽。它是 slotdef 条目的包装器。
当我们调用由 add_operators() 添加的特殊方法时,我们调用了包装器描述符。当我们调用包装器描述符时,它会调用包装器函数。包装器描述符将传递给包装器函数的参数与我们传递给特殊方法的相同参数加上包装槽。最后,包装函数调用包装槽。
让我们看看实现 nb_add 槽的内置类型如何获得它的 add() 和 radd() 特殊方法。回忆一下nb_add对应的slotdef条目:
static slotdef slotdefs[] = {
// ...
// {name, offset, function,
// wrapper, doc}
//
{"__add__", offsetof(PyHeapTypeObject, as_number.nb_add), (void *)(slot_nb_add),
wrap_binaryfunc_l, PyDoc_STR("__add__" "($self, value, /)\n--\n\nReturn self" "+" "value.")},
{"__radd__", offsetof(PyHeapTypeObject, as_number.nb_add), (void *)(slot_nb_add),
wrap_binaryfunc_r, PyDoc_STR("__radd__" "($self, value, /)\n--\n\nReturn value" "+" "self.")},
// ...
}
wrap_binaryfunc_l() 和 wrap_binaryfunc_r() 都采用两个操作数和一个包装槽作为它们的参数。唯一的区别是他们如何称呼插槽:
- wrap_binaryfunc_l(x, y, slot_func) 调用 slot_func(x, y)
- wrap_binaryfunc_r(x, y, slot_func) 调用 slot_func(y, x)。
这个调用的结果就是我们调用特殊方法时得到的结果。
总结¶
今天,我们揭开了 Python 最神奇的一面的神秘面纱。我们已经了解到,Python 对象的行为是由对象类型的槽位决定的。可以显式指定静态定义类型的槽, 任何类型都可以从其祖先那里继承一些槽。真正的见解是类的槽是由 CPython 基于定义的特殊方法自动设置的。 CPython 也做相反的事情。如果类型实现了相应的槽, 它会向类型的字典中添加特殊方法。
我们学到了很多。然而,Python 对象系统是一个如此庞大的主题,至少有同样多的内容有待涵盖。例如,我们还没有真正讨论过属性是如何工作的。这就是我们下次要做的。