我们知道所有类型对象在底层都由结构体 PyTypeObject 实例化得到,但内部字段接收的值不同,得到的类型对象就不同。类型对象不同,那么实例对象的表现就不同,这也正是一种对象区别于另一种对象的关键所在。
比如 PyLong_Type 的 tp_iter 是空,那么整数就不是可迭代对象,而 PyList_Type 的 tp_iter 不是空,那么列表就是可迭代对象。再比如 PyLong_Type 和 PyFloat_Type,虽然内部都实现了 tp_hash,但它们是不同的类型,所以整数和浮点数的哈希值计算方式也不一样。
因此类型对象决定了实例对象的行为,比如能否调用、能否计算哈希值、能否迭代等等,这些都由类型对象决定。
PyTypeObject 里面定义了很多函数指针,比如 tp_call、tp_hash 等等,它们可能指向某个具体的函数,也可能为空。这些函数指针可以看做是类型对象所定义的操作,这些操作决定了其实例对象在运行时的行为。
class A:# tp_newdef __new__(cls, *args, **kwargs): pass# tp_initdef __init__(self): pass# tp_calldef __call__(self): pass# tp_getattrdef __getattr__(self, attr): pass# tp_setattrdef __setattr__(self, key, value): pass......
像 tp_call、tp_hash、tp_new 等字段会直接对应 Python 里的魔法函数,它们以双下划线开头、以双下划线结尾。但除了魔法函数之外,每种类型还可以有很多自定义的成员函数。
# 自定义 foo 和 barclass A: def foo(self): pass def bar(self): pass# 当然内置类型也是如此# 像 str 定义了 join、split、upperprint(str.join)print(str.split)print(str.upper)# 像 list 定义了 append、extend,insertprint(list.append)print(list.extend)print(list.insert)
这些自定义的函数会一起保存在类型对象的 tp_methods 里面,负责让实例对象更具有表现力。需要补充的是,类型对象里面定义的是函数,也叫成员函数,实例对象在获取之后会自动包装成方法。
所以实例对象能调用的方法都定义在类型对象里面,并且通过实例调用本质上就是一个语法糖,但用起来更加优雅。假设有一个类 A,实例对象为 a,那么 a.some() 底层会转成 A.some(a),至于这背后的细节后续再聊。
但除了以上这些,PyTypeObject 还提供了三个字段。
图片
每个字段各自指向一个结构体实例,结构体实例中有大量的字段,这些字段也是函数指针,指向了具体的函数。所以它们也被称为方法簇,分别应用于如下操作。
我们以 tp_as_number 为例,它指向 PyNumberMethods 类型的结构体实例,那么这个结构体长什么样子呢?
// Include/cpython/object.htypedef struct {// add,对应 + 操作符,如 a + bbinaryfunc nb_add;// sub,对应 - 操作符,如 a - bbinaryfunc nb_subtract;// mul,对应 * 操作符,如 a * bbinaryfunc nb_multiply;// mod,对应 % 操作符,如 a % bbinaryfunc nb_remainder;// divmod,对应 divmode 函数,如 divmod(a, b)binaryfunc nb_divmod;// power,对应 ** 操作符,如 a ** bternaryfunc nb_power;// neg,对应 - 操作符,如 -aunaryfunc nb_negative;// pos,对应 + 操作符,如 +aunaryfunc nb_positive;// abs,对应 abs 函数,如 abs(a)unaryfunc nb_absolute;// bool,如 bool(a)inquiry nb_bool;// invert,对应 ~ 操作符,如 ~aunaryfunc nb_invert;// lshift,对应 << 操作符,如 a << bbinaryfunc nb_lshift;// rshift,对应 >> 操作符,如 a >> bbinaryfunc nb_rshift;// and,对应 & 操作符,如 a & bbinaryfunc nb_and;// xor,对应 ^ 操作符,如 a ^ bbinaryfunc nb_xor;// or,对应 | 操作符,如 a | bbinaryfunc nb_or;// int,如 int(a)unaryfunc nb_int;// ...} PyNumberMethods;
你看到了什么?是不是想到了 Python 里面的魔法方法,所以它们也被称为方法簇。
在 PyNumberMethods 这个方法簇里面定义了作为一个数值应该支持的操作,如果一个对象能被视为数值,比如整数,那么在其对应的类型对象 PyLong_Type 中,tp_as_number->nb_add 就指定了该对象进行加法操作时的具体行为。
同样,PySequenceMethods 和 PyMappingMethods 中分别定义了作为一个序列对象和映射对象应该支持的行为,这两种对象的典型例子就是 list 和 dict。
所以,只要类型对象提供相关操作,实例对象便具备对应的行为,因为实例对象所调用的方法都是由类型对象提供的。
class Girl:
class Girl:def __init__(self, name, age): self.name = name self.age = agedef say(self): passdef cry(self): pass
实例对象的属性字典,只包含了一些在 init 里面设置的属性而已,而实例能够调用的 say、cry 都是定义在类型对象中的。
因此一定要记住:类型对象定义的操作,决定了实例对象的行为。
class Int(int): def __getitem__(self, item): return itema = Int(1)b = Int(2)print(a + b) # 3print(a["你好"]) # 你好
继承自 int 的 Int 在实例化之后自然是一个数值对象,但看上去 a[""] 是一个类似于字典才具有的行为,那为什么可以实现呢?
原因就是我们重写了 getitem 这个魔法函数,该方法在底层对应 PyMappingMethods 中的 mp_subscript 操作,因此最终 Int 实例对象表现的像一个字典一样。
归根结底就在于这几个方法簇都只是 PyTypeObject 的一个字段罢了,默认使用 PyTypeObject 结构体创建的 PyLong_Type 所生成的实例对象是不具备列表和字典的属性特征的。但我们通过继承 PyLong_Type,同时指定 getitem,使得构建出来的类型对象所生成的实例对象,同时具备多种属性特征,就是因为解释器支持这种做法。
自定义的类在底层也是 PyTypeObject 结构体实例,而在继承 int 的时候,将其内部定义的 PyNumberMethods 方法簇也继承了下来,而我们又单独实现了 PyMappingMethods 中的 mp_subscript。所以自定义类 Int 的实例对象具备了整数的全部行为,以及字典的部分行为(因为我们只实现了 getitem)。
我们再通过 PyLong_Type 实际考察一下:
图片
整数对象显然不支持序列和映射操作,所以在创建 PyLong_Type 时,字段 tp_as_sequence 和 tp_as_mapping 就是 0,相当于空。但整数明显支持数值型操作,所以实现了 tp_as_number。
而 tp_as_number 字段被赋值为 long_as_number,看一下它长什么样。
图片
里面的 long_add、long_sub、long_mul 等等显然都是已经定义好的函数指针,在创建 PyNumberMethods 结构体实例 long_as_number 的时候,分别赋值给了字段 nb_add、nb_substract、nb_multiply 等等。
创建完整数相关操作的 PyNumberMethods 结构体实例 long_as_number 之后,再将其指针交给 PyLong_Type 的 tp_as_number 字段。
然后整数在操作的时候,比如相加,会先通过 变量->ob_type->tp_as_number->nb_add 获取该操作对应的函数指针,其中 int 类型对象的 tp_as_number 字段的值是 &long_as_number,因此获取其字段 nb_add 的时候,拿到的就是 long_add 函数指针,然后调用。
同理 float 类型里的 tp_as_number 则被赋值成了 &float_as_number,获取 nb_add 字段的时候,拿到的就是 float_add 函数指针。不同类型的对象的行为不同,它们都有属于自己的一组方法簇。
最后再画一张图总结一下,假设有两个变量,分别是 e = 2.71 和 num = 666。
所以对象的行为是由其类型对象定义的操作所决定的,比如一个对象可以计算长度,那么它的类型对象要实现 len;一个对象可以转成整数,那么它的类型对象要实现 int 或 index。
class A: def __len__(self): return 123 def __int__(self): return 456a = A()print(len(a)) # 123print(int(a)) # 456# len(a) 在底层会转成 A.__len__(a)# int(a) 在底层会转成 A.__int__(a)print(A.__len__(a)) # 123print(A.__int__(a)) # 456
a = A()print(len(a)) # 123print(int(a)) # 456
print(A.len(a)) # 123print(A.int(a)) # 456总之核心就是一句话:类型对象定义了哪些操作,决定了实例对象具有哪些行为。
本文链接:http://www.28at.com/showinfo-26-91364-0.htmlPython 对象的行为是怎么区分的?
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com