Python创建一个对象,比如PyFloatObject,会分配内存并进行初始化。然后内部统一使用泛型指针 PyObject* 来保存和维护这个对象,而不是PyFloatObject *。而不是PyFloatObject *。通过PyObject *保存和维护对象,可以实现更加抽象的上层逻辑,而不用关心对象的实际类型和实现细节。
Py_hash_t
PyObject_Hash(PyObject *V);
该函数可以计算任意对象的哈希值,而不用关心对象的类型是啥,它们都可以使用这个函数。
但是不同的类型对象,行为也是千差万别,哈希值的计算方式也是如此,那PyObject_Hash函数是如何解决这个问题的呢?不用想,因为元信息存储在对应的类型对象中,所以肯定会通过其ob_type拿到指向的类型对象。而类型对象中有一个成员叫tp_hash,它是一个函数指针,指向的函数专门用来计算其实例对象的哈希值。所以我们看一下PyObject_Hash的函数定义,看看它内部都做了什么,该函数位于Object/Object.c中。
Py_hash_t
PyObject_Hash(PyObject *v)
{
/* Py_TYPE是一个宏,用来获取PyObject *内部的ob_type */
PyTypeObject *tp = Py_TYPE(v);
/* 获取对应的类型对象内部的tp_hash */
/* tp_hash是一个函数指针,对应__hash__ */
if (tp -> tp_hash != NULL)
/* 如果tp_hash不为空,证明确实指向了具体的hash函数
* 那么拿到函数指针之后,通过*获取对应的函数
* 然后将PyObject *传进去计算哈希值,返回
*/
return (*tp -> tp_hash)(v);
/* 走到这里说明tp_hash为空,但这存在两种可能
* 1. 该类型对象可能还未完全初始化, 导致tp_hash暂时为空
* 2. 该类型本身就不支持其 "实例对象" 被哈希
*/
/* 如果是第一种情况,那么它的tp_dict(属性字典)一定为空
* tp_dict是动态设置的,它为空,是类型对象没有完全是初始化的重要特征
* 但如果tp_dict不为空,说明类型对象一定已经被完全初始化了
* 所以此时tp_hash要是还为空,就真的说明该类型不支持实例对象被哈希
*/
if (tp->tp_dict == NULL) {
/* 属性字典为空,那么先进行类型的初始化 */
if (PyType_Ready(tp) < 0)
return -1
/* 然后再看是否tp_hash是否为空,为空的话,说明不支持哈希
* 不为空则调用对应的哈希函数
*/
if (tp->tp_hash != NULL)
return (*tp->tp_hash)(v);
}
/* 说明该对象不可以被hash */
return PyObject_HashNotImplemented(v);
}
函数首先通过ob_type指针找到对象的类型,然后通过类型对象的tp_hash函数指针调用对应的哈希计算函数。所以PyObject_Hash根据对象的类型不同,然后调用不同的哈希函数,这就是实现了多态。我们再以Python为例:
# 计算 v 的哈希值
hash(v)
# 而 hash(v) 等价于
v.__class__.hash(v)
# 如果 v 是一个列表,那么就是 list.__hash__(v)
# 如果 v 是一个字符串,那么就是 str.__hash__(v)
如果一个对象支持哈希操作,那么它的类型对象当中一定定义了 hash 方法,通过 v.class 就可以获取它的类型对象,然后将 v 作为参数调用 hash 即可。
所以通过ob_type字段,Python 在 C 语言的层面实现了对象的多态特性,思路跟 C++ 中的虚表指针有着异曲同工之妙。
有人觉得PyObject_Hash函数的源码写的不是很精简,比如一开始已经判断过内部的 tp_hash 是否为 NULL,然后在下面又判断了一次。那么可不可以先判断 tp_dict 是否为NULL,为 NULL 进行初始化,然后再判断 tp_hash 是否NULL,不为 NULL 的话执行 tp_hash。这样的话,代码会变得精简很多。
答案是可以的,而且这种方式似乎更直观,但是效率上不如源码。因为我们这种方式的话,无论是什么对象,都需要判断其类型对象中 tp_dict 和 tp_hash 是否为 NULL。而源码中先判断 tp_hash 是否为NULL,不为 NULL 的话就不需要再判断 tp_dict 了。所以对于已经初始化(tp_hash 不为 NULL)的类型对象,源码中少了一次对 tp_dict 是否为 NULL 的判断,效率会更高。
而这种行为叫做 CPython 的快分支,并且 CPython 中还有很多其它的快分支,快分支的特点就是命中率极高,可以尽早做出判断、尽早处理。回到当前这个场景,只有当类型对象未被初始化的时候,才会不走快分支;而一旦初始化完毕,那么后续就都走快分支。
也就是说,快分支只有在第一次调用的时候才可能不会命中,其余情况都是命中,因此没有必要每次都对 tp_dict 进行判断。因此源码的设计是非常合理的,我们在后面分析函数调用的时候,也会看到很多类似于这样的快分支。
了解完对象的多态性,我们再来说说对象的行为。虽然 Python 的类型对象和实例对象都属于对象,但我们更关注的是实例对象的行为。
而不用的对象的行为不同,比如哈希值的计算方式,它是由类型对象的tp_hash成员决定的。但除了 tp_hash,PyTypeObject 中还定义了很多其它的函数指针,这些指针最终都会指向某个函数,或者为空表示不支持该操作。
这些函数指针可以看做是类型对象所定义的操作,这些操作决定了其实例对象在运行时的行为。虽然所有类型对象在底层都是由结构体PyTypeObject实例化得到的,但内部成员接收的值不同,得到的类型对象在底层都是结构体PyTypeObject实例化得到的,但内部成员接收的值不同,得到的类型对象就不同;类型对象不同,导致其实例对象的行为就不同,这也正是一种对象区别于另一种对象的关键所在。
比如 int 和 str 内部都有 hash,但它们是不同的类型,因此哈希值的计算方式也不同。
而根据支持的操作不同,Python 中可以将对象进行以下分类:
这三种操作,在 PyTypeObject 中分别对应三个指针。每个指针指向一个结构体实例,这个结构体实例中有大量的成员,成员也是函数指针,指向了具体的函数。我们回顾一下 PyTypeObject 的定义:
typedef struct _typeobject {
PyObject_VAR_HEAD
const char *tp_name;
// .......
PyNumberMethods *tp_as_number; // 数值型相关操作
PySequenceMethods *tp_as_sequence; // 序列型相关操作
PyMappingMethods *tp_as_mapping; // 映射型相关操作
// ......
} PyTypeObject;
PyNumberMethods、PySequenceMethods、PyMappingMethods 都是结构体,里面每一个成员也都是函数指针类型,指针指向的函数就是相应的操作。我们以 PyNumberMethods 为例,看看它是怎么定义的?
typedef struct {
binaryfunc nb_add;
binaryfunc nb_subtract;
binaryfunc nb_multiply;
binaryfunc nb_remainder;
binaryfunc nb_divmod;
ternaryfunc nb_power;
unaryfunc nb_negative;
unaryfunc nb_positive;
unaryfunc nb_absolute;
inquiry nb_bool;
unaryfunc nb_invert;
binaryfunc nb_lshift;
binaryfunc nb_rshift;
//......
//......
binaryfunc nb_inplace_matrix_multiply;
} PyNumberMethods;
看到上面这段代码,是不是可以联想到Python里面的魔法方法,所以他们也被称为方法簇。
在PyNumberMethods这个方法簇里面定义了作为一个数值应该支持的操作,如果一个对象能被视为数值,比如整数,那么在其对应的类型对象PyLong_Type中,tp_as_number -> nb_add 就指定了该对象进行加法操作是的具体行为。
同样,PySequenceMethods 和 PyMappingMethods中分别定义了作为一个序列对象和映射对象应该支持的行为,这两种对象的典型例子就是list 和 dict。
所以只要是类型对象提供相关操作,实例对象便具备对应的行为,因为实例对象对象所调用的方法都是由类型对象提供的。
class Girl:
def __init__(self, name, age):
self.name = name
self.age = age
def say(self):
pass
def cry(self):
pass
g = Girl("奈文摩尔", 16)
print(g.__dict__) # {'name': '奈文摩尔', 'age': 16}
print("say" in Girl.__dict__) # True
print("cry" in Girl.__dict__) # True
我们看到实例对象的属性字典里面只有在__init__里面设置的一些属性而已,而实例能够调用的say,cry都是定义在类型对象中的。
因此一定要记住:类型对象定义的操作,决定了实例对象的行为。
class Int(int):
def __getitem__(self, item):
return item
a = Int(1)
b = Int(2)
print(a + b) # 3
print(a["你好"]) # 你好
继承自 int 的 Int 在实例化之后自然是一个数值对象,但是看上去a[“”]这种操作是一个类似于字典才支持的操作,为什么可以实现呢?
原因就是我们重写了__getitem__ 这个魔法方法,该方法在底层对应PyMappingMethods中的mp_subscript操作。最终Int实例对象表现得像一个字典一样。
归根结底就在于这几个方法簇都只是 PyTypeObject 的一个成员罢了,默认使用 PyTypeObject 结构体创建的 PyLong_Type 所生成的实例对象是不具备列表和字典的属性特征的。但是我们继承PyLong_Type,同时指定__getitem__,使得我们自己构建出来的类型对象所生成的实例对象,同时具备多种属性特征,就是因为解释器这种做法。
我们自定义的类在底层也是 PyTypeObject 结构体实例,而在继承 int 的时候,将其内部定义的 PyNumberMethods 方法簇也继承了下来,而我们又单独实现了 PyMappingMethods中的mp_subscript。所以自定义类Int的实例对象具备了整数的全部行为,以及字典的部分行为(因为我们只实现了__getitem__)。
我们通过PyFloat_Type实际考察一下:
PyTypeObject PyFloat_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"float",
sizeof(PyFloatObject),
//......
&float_as_number, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
//......
};
我们看到了该类型对象float在创建时,给成员tp_as_number传入了一个float_as_number指针。那么这个 float_as_number就是PyNumberMethods结构体实例,而其内部的每一个成员都是指向了浮点数运算函数指针。
static PyNumberMethods float_as_number = {
float_add, /* nb_add */
float_sub, /* nb_subtract */
float_mul, /* nb_multiply */
float_rem, /* nb_remainder */
float_divmod, /* nb_divmod */
float_pow, /* nb_power */
// ...
};
里面的 float_add、float_sub、float_mul 等等显然都是已经定义好的指针,然后创建PyNumberMethods结构体实例float_as_number的时候,分别复制给了成员nb_add、nb_substract、nb_multiply 等等。
而创建完浮点数相关操作的PyNumberMethods结构体实例float_as_number之后,将其指针交给PyFloat_Type中的tp_as_number成员。而浮点数相加的时候,会先通过**变量 -> ob_type -> tp_as_number -> nb_add **获取该操作对应的函数指针,其中浮点类型对象的tp_as_number成员的值是&float_as_number,因此在获取其成员nb_add的时候,拿到的就是float_add指针,然后调用float_add函数。
整个过程还是不难理解的,另外我们在PyFloat_Type 中看到tp_as_sequence和tp_as_mapping这两个成员接收到的值则不是一个函数指针,而是0(相当于空)。
因此浮点数不支持序列型操作和映射型操作,比如:pi = 3.14,我们无法使用len计算长度、无法通过索引或者切片获取指定位置的值、无法通过key获取value,这和我们使用Python时候的表现是一致的。
以上就是对象的多态性和行为,多态比较简单,是通过泛型指针 PyObject *和ob_type实现的。
而对象的行为是由其类型对象内部定义的操作所决定的,比如一个对象可以计算长度,那么它的类型对象内部就要实现__len__; 一个对象可以转成整数,那么它的类型对象内部要实现__int__或__index__。
class A:
def __len__(self):
return 123
def __int__(self):
return 456
a = A()
print(len(a)) # 123
print(int(a)) # 456
# 而 len(a) 在底层会调用 A.__len__(a)
# int(a) 在底层会调用 A.__int__(a)
print(A.__len__(a)) # 123
print(A.__int__(a)) # 456
# 注意:len(a)在底层调用的是 A.__len__(a),而不是 a.__len__()
# 举个栗子
print(a.__len__(), len(a)) # 123 123
a.__dict__["__len__"] = "哼哼哼"
print(a.__len__, len(a)) # 哼哼哼 123
# 其他内置函数同理
# 而且实例调用类型对象中定义的方式时,事实上也是通过类型对象调用的
# a.some_method(*args) 只是 A.some_method(a, *args)的一个语法糖
总之核心就是一句话:类型对象定义了哪些操作,决定了实例对象具备哪些行为。