我们知道对象被创建,主要有两种方式,一种是通过python/c api,另一种是通过调用类型对象。对于内置类型的实例对象而言,这两种方式都是支持的,比如列表,我们即可以通过[]创建,也可以通过list(),前者是python/c api,后者是调用类型对象。
但对于自定义类的实例对象而言,我们只能通过调用类型对象的方式来创建。而一个对象如果可以被调用,那么这个对象就是callable,否则就不是callable。
而决定一个对象是不是callable,就取决于其对应的类型对象中是否定义了某个方法。如果从 python 的角度看的话,这个方法就是 __call__,从解释器角度看的话,这个方法就是 tp_call。
从 python 的角度看对象的调用
调用 int、str、tuple 可以创建一个整数、字符串、元组,调用自定义的类也可以创建出相应的实例对象,说明类型对象是可调用的,也就是callable。那么这些类型对象(int、str、tuple、class等等)的类型对象(type)内部一定有 __call__ 方法。
# int可以调用 # 那么它的类型对象、也就是元类(type), 内部一定有__call__方法 print(hasattr(type, __call__))# true # 而调用一个对象,等价于调用其类型对象的 __call__ 方法 # 所以 int(3.14)实际就等价于如下 print(type.__call__(int, 3.14))# 3
注意:这里描述的可能有一些绕,我们说 int、str、float 这些都是类型对象(简单来说就是类),而 123、你好、3.14 是其对应的实例对象,这些都没问题。但type是不是类型对象,显然是的,虽然我们称呼它为元类,但它也是类型对象,如果 print(type) 显示的也是一个类。
那么相对 type 而言,int、str、float 是不是又成了实例对象呢?因为它们的类型是 type。
所以 class 具有二象性:
如果站在实例对象(如:123、satori、[]、3.14)的角度上,它是类型对象 如果站在 type 的角度上,它是实例对象
同理 type 的类型是也是 type,那么 type 既是 type 的类型对象,type 也是 type 的实例对象。虽然这里描述的会有一些绕,但应该不难理解,并且为了避免后续的描述出现歧义,这里我们做一个申明:
整数、浮点数、字符串等等,我们称之为实例对象
int、float、str、dict,以及我们自定义的类,我们称之为类型对象
type 虽然也是类型对象,但我们称它为元类
所以 type 的内部有 __call__ 方法,那么说明类型对象都是可调用的,因为调用类型对象就是调用 type 的 __call__ 方法。而实例对象能否调用就不一定了,这取决于它的类型对象中是否定义了 __call__ 方法,因为调用一个对象,本质上是执行其类型对象内部的 __call__ 方法。
class a: pass a = a() # 因为我们自定义的类 a 里面没有 __call__ # 所以 a 是不可以被调用的 try: a() except exception as e: # 告诉我们 a 的实例对象不可以被调用 print(e)# 'a' object is not callable # 如果我们给 a 设置了一个 __call__ type.__setattr__(a, __call__, lambda self: 这是__call__) # 发现可以调用了 print(a())# 这是__call__
我们看到这就是动态语言的特性,即便在类创建完毕之后,依旧可以通过type进行动态设置,而这在静态语言中是不支持的。所以type是所有类的元类,它控制了我们自定义类的生成过程,type这个古老而又强大的类可以让我们玩出很多新花样。
但是对于内置的类,type是不可以对其动态增加、删除或者修改属性的,因为内置的类在底层是静态定义好的。因为从源码中我们看到,这些内置的类、包括元类,它们都是pytypeobject对象,在底层已经被声明为全局变量了,或者说它们已经作为静态类存在了。所以type虽然是所有类型对象的元类,但是只有在面对我们自定义的类,type才具有增删改的能力。
而且我们也解释过,python 的动态性是解释器将字节码翻译成 c 代码的时候动态赋予的,因此给类动态设置属性或方法只适用于动态类,也就是在 py 文件中使用 class 关键字定义的类。
而对于静态类、或者编写扩展模块时定义的扩展类(两者是等价的),它们在编译之后已经是指向 c 一级的数据结构了,不需要再被解释器解释了,因此解释器自然也就无法在它们身上动手脚,毕竟彪悍的人生不需要解释。
try: type.__setattr__(dict, __call__, lambda self: 这是__call__) except exception as e: print(e)# can't set attributes of built-in/extension type 'dict'
我们看到抛异常了,提示我们不可以给内置/扩展类型dict设置属性,因为它们绕过了解释器解释执行这一步,所以其属性不能被动态设置。
同理其实例对象亦是如此,静态类的实例对象也不可以动态设置属性:
class girl: pass g = girl() g.name = 古明地觉 # 实例对象我们也可以手动设置属性 print(g.name)# 古明地觉 lst = list() try: lst.name = 古明地觉 except exception as e: # 但是内置类型的实例对象是不可以的 print(e)# 'list' object has no attribute 'name'
可能有人奇怪了,为什么列表不行呢?答案是内置类型的实例对象没有__dict__属性字典,因为相关属性或方法底层已经定义好了,不可以动态添加。如果我们自定义类的时候,设置了__slots__,那么效果和内置的类是相同的。
当然了,我们后面会介绍如何通过动态修改解释器来改变这一点,举个栗子,不是说静态类无法动态设置属性吗?下面我就来打自己脸:
import gc try: type.__setattr__(list, ping, pong) except typeerror as e: print(e)# can't set attributes of built-in/extension type 'list' # 我们看到无法设置,那么我们就来改变这一点 attrs = gc.get_referents(tuple.__dict__)[0] attrs[ping] = pong print(().ping)# pong attrs[append] = lambda self, item: self + (item,) print( ().append(1).append(2).append(3) )# (1, 2, 3)
我脸肿了。好吧,其实这只是我们玩的一个小把戏,当我们介绍完整个 cpython 的时候,会来专门聊一聊如何动态修改解释器。比如:让元组变得可修改,让 python 真正利用多核等等。
从解释器的角度看对象的调用
我们以内置类型 float 为例,我们说创建一个 pyfloatobject,可以通过3.14或者float(3.14)的方式。前者使用python/c api创建,3.14直接被解析为 c 一级数据结构,也就是pyfloatobject实例;后者使用类型对象创建,通过对float进行一个调用、将3.14作为参数,最终也得到指向c一级数据结构pyfloatobject实例。
python/c api的创建方式我们已经很清晰了,就是根据值来推断在底层应该对应哪一种数据结构,然后直接创建即可。我们重点看一下通过类型调用来创建实例对象的方式。
如果一个对象可以被调用,它的类型对象中一定要有tp_call(更准确的说成员tp_call的值是一个函数指针,不可以是0),而pyfloat_type是可以调用的,这就说明pytype_type内部的tp_call是一个函数指针,这在python的层面上我们已经验证过了,下面我们再来通过源码看一下。
//typeobject.c 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 */ //... /* tp_hash */ (ternaryfunc)type_call, /* tp_call */ //... }
我们看到在实例化pytype_type的时候pytypeobject内部的成员tp_call被设置成了type_call。这是一个函数指针,当我们调用pyfloat_type的时候,会触发这个type_call指向的函数。
因此 float(3.14) 在c的层面上等价于:
(&pyfloat_type) -> ob_type -> tp_call(&pyfloat_type, args, kwargs); // 即: (&pytype_type) -> tp_call(&pyfloat_type, args, kwargs); // 而在创建 pytype_type 的时候,给 tp_call 成员传递的是 type_call // 因此最终相当于 type_call(&pyfloat_type, args, kwargs)
如果用 python 来演示这一过程的话:
# float(3.14),等价于 f1 = float.__class__.__call__(float, 3.14) # 等价于 f2 = type.__call__(float, 3.14) print(f1, f2)# 3.14 3.14
这就是 float(3.14) 的秘密,相信list、dict在实例化的时候是怎么做的,你已经猜到了,做法是相同的。
# lst = list(abcd) lst = list.__class__.__call__(list, abcd) print(lst)# ['a', 'b', 'c', 'd'] # dct = dict([(name, 古明地觉), (age, 17)]) dct = dict.__class__.__call__(dict, [(name, 古明地觉), (age, 17)]) print(dct)# {'name': '古明地觉', 'age': 17}
最后我们来围观一下 type_call 函数,我们说 type 的 __call__ 方法,在底层对应的是 type_call 函数,它位于object/typeobject.c中。
static pyobject * type_call(pytypeobject *type, pyobject *args, pyobject *kwds) { // 如果我们调用的是 float // 那么显然这里的 type 就是 &pyfloat_type // 这里是声明一个pyobject * // 显然它是要返回的实例对象的指针 pyobject *obj; // 这里会检测 tp_new是否为空,tp_new是什么估计有人已经猜到了 // 我们说__call__对应底层的tp_call // 显然__new__对应底层的tp_new,这里是为实例对象分配空间 if (type->tp_new == null) { // tp_new 是一个函数指针,指向具体的构造函数 // 如果 tp_new 为空,说明它没有构造函数 // 因此会报错,表示无法创建其实例 pyerr_format(pyexc_typeerror, cannot create '%.100s' instances, type->tp_name); return null; } //通过tp_new分配空间 //此时实例对象就已经创建完毕了,这里会返回其指针 obj = type->tp_new(type, args, kwds); //类型检测,暂时不用管 obj = _py_checkfunctionresult((pyobject*)type, obj, null); if (obj == null) return null; //我们说这里的参数type是类型对象,但也可以是元类 //元类也是由pytypeobject结构体实例化得到的 //元类在调用的时候执行的依旧是type_call //所以这里是检测type指向的是不是pytype_type //如果是的话,那么实例化得到的obj就不是实例对象了,而是类型对象 //要单独检测一下 if (type == &pytype_type && pytuple_check(args) && pytuple_get_size(args) == 1 && (kwds == null || (pydict_check(kwds) && pydict_get_size(kwds) == 0))) return obj; //tp_new应该返回相应类型对象的实例对象(的指针) //但如果不是,就直接将这里的obj返回 //此处这么做可能有点难理解,我们一会细说 if (!pytype_issubtype(py_type(obj), type)) return obj; //拿到obj的类型 type = py_type(obj); //执行 tp_init //显然这个tp_init就是__init__函数 //这与python中类的实例化过程是一致的。 if (type->tp_init != null) { //将tp_new返回的对象作为self,执行 tp_init int res = type->tp_init(obj, args, kwds); if (res tp_new(type, args, kwds); if (type->tp_init != null) { //将__new__返回的实例obj,和args、kwds组合起来 //一起传给 __init__ //其中 obj 会传给 self, int res = type->tp_init(obj, args, kwds); //...... return obj; }
所以源码层面表现出来的,和我们在 python 层面看到的是一样的。
小结
到此,我们就从 python 和解释器两个层面了解了对象是如何调用的,更准确的说我们是从解释器的角度对 python 层面的知识进行了验证,通过 tp_new 和 tp_init 的关系,来了解 __new__ 和 __init__ 的关系。
另外,对象调用远不止我们目前说的这么简单,更多的细节隐藏在了幕后,只不过现在没办法将其一次性全部挖掘出来。
以上就是源码探秘:python 中对象是如何被调用的?的详细内容。
