楔子
在上一篇文章中,我们分析了对象是如何创建的,主要有两种方式,一种是通过特定类型 API,另一种是通过调用类型对象。
对于内置类型的实例对象而言,这两种方式都是支持的,比如列表,我们既可以通过 [ ] 创建,也可以通过 list() 创建,前者是列表的特定类型 API,后者是调用类型对象。
但对于自定义类的实例对象而言,我们只能通过调用类型对象的方式来创建。一个对象如果可以被调用,那么这个对象就是 callable,否则就不是 callable。而决定一个对象是不是 callable,则取决于它的类型对象。
- 从 Python 的角度看,如果对象是 callable,那么它的类型对象一定实现了 __call__ 函数;
- 从解释器的角度看,如果对象是 callable,那么它的类型对象的 tp_call 字段一定不为空。
从 Python 的角度看对象的调用
调用 int 可以创建一个整数,调用 str 可以创建一个字符串,调用 tuple 可以创建一个元组,调用自定义的类也可以创建出相应的实例对象,这就说明类型对象是可调用的,也就是 callable。
既然类型对象可调用,那么类型对象的类型对象(type)内部一定实现了 __call__ 函数。
# int 可以调用,那么它的类型对象、也就是元类(type) # 内部一定实现了 __call__ 函数 print(hasattr(type, "__call__")) # True # 而调用一个对象,等价于调用其类型对象的 __call__ 函数 # 所以 int(2.71) 实际就等价于如下 print(type.__call__(int, 2.71)) # 2
我们说 int、str、float 这些都是类型对象(简单来说就是类),而 123、"你好"、2.71 是其对应的实例对象,这些都没问题。但相对 type 而言,int、str、float 是不是又成了实例对象呢?因为它们的类型是 type。
所以 class 具有二象性:
- 如果站在实例对象(如:123、"satori"、2.71)的角度上,它是类型对象;
- 如果站在 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, "ping", "pong") except Exception as e: print(e) """ cannot set 'ping' attribute of immutable type 'dict' """ try: type.__setattr__(list, "ping", "pong") except Exception as e: print(e) """ cannot set 'ping' attribute of immutable type 'list' """
同理其实例对象亦是如此,静态类的实例对象也不可以动态设置属性:
lst = list() try: lst.name = "古明地觉" except Exception as e: print(e) # 'list' object has no attribute 'name'
在介绍 PyTypeObject 结构体的时候我们说过,静态类的实例对象可以绑定哪些属性,已经写死在 tp_members 字段里面了。
从解释器的角度看对象的调用
以内置类型 list 为例,我们说创建一个列表,可以通过 [ ] 或者 list() 的方式。前者使用列表的特定类型 API 创建,[ ] 会被直接解析成 C 一级的数据结构,也就是 PyListObject 实例;后者使用类型对象创建,对 list 进行调用,最终也得到指向 C 一级的数据结构 PyListObject 实例。
第一种方式我们已经很熟悉了,就是根据值来推断在底层应该对应哪一种数据结构,然后直接创建即可,因为解释器对内置的数据结构了如指掌。我们重点来看第二种方式,也就是通过调用类型对象去创建实例对象。
如果一个对象可以被调用,那么它的类型对象中一定要有 tp_call,更准确的说是 tp_call 字段的值是一个具体的函数指针,而不是 0。由于 PyList_Type 是可以调用的,这就说明 PyType_Type 内部的 tp_call 是一个函数指针,这在 Python 的层面我们已经验证过了,下面再来通过源码看一下。
在创建 PyType_Type 的时候,PyTypeObject 内部的 tp_call 字段被设置成了 type_call。所以当我们调用 PyList_Type 的时候,会执行 type_call 函数。
因此 list() 在 C 的层面上等价于:
(&PyList_Type)->ob_type->tp_call(&PyList_Type, args, kwargs); // 即: (&PyType_Type)->tp_call(&PyList_Type, args, kwargs); // 而在创建 PyType_Type 的时候,给 tp_call 字段传递的是 type_call // 因此最终相当于 type_call(&PyList_Type, args, kwargs)
如果用 Python 来演示这一过程的话:
# 以 list("abcd") 为例,它等价于 lst1 = list.__class__.__call__(list, "abcd") # 等价于 lst2 = type.__call__(list, "abcd") print(lst1) # ['a', 'b', 'c', 'd'] print(lst2) # ['a', 'b', 'c', 'd']
这就是 list() 的秘密,相信其它类型在实例化的时候是怎么做的,你已经知道了,做法是相同的。
# dct = dict([("name", "古明地觉"), ("age", 17)]) dct = dict.__class__.__call__( dict, [("name", "古明地觉"), ("age", 17)] ) print(dct) # {'name': '古明地觉', 'age': 17} # buf = bytes("hello world", encoding="utf-8") buf = bytes.__class__.__call__( bytes, "hello world", encoding="utf-8" ) print(buf) # b'hello world'
当然,目前还没有结束,我们还需要看一下 type_call 的源码实现。
type_call 源码解析
调用类型对象,本质上会调用 type.__call__,在底层对应 type_call 函数,因为 PyType_Type 的 tp_call 字段被设置成了 type_call。当然调用 type 也是如此,因为 type 的类型还是 type。
那么这个 type_call 都做了哪些事情呢?
static PyObject * type_call(PyTypeObject *type, PyObject *args, PyObject *kwds) { // 参数 type 表示类型对象或者元类,假设调用的是 list,那么它就是 &PyList_Type // 参数 args 和 kwds 表示位置参数和关键字参数,args 是元组,kwds 是字典 // 创建的实例对象,当然也可能是类型对象,取决于参数 type PyObject *obj; // 线程状态对象,后续介绍线程的时候会细说 // 此处的线程状态对象是用来设置异常的 PyThreadState *tstate = _PyThreadState_GET(); // 如果参数 type 是 &PyType_Type,也就是 Python 中的元类 if (type == &PyType_Type) { // 那么它只能接收一个位置参数(查看对象类型)或三个位置参数(动态创建类) Py_ssize_t nargs = PyTuple_GET_SIZE(args); // 获取位置参数的个数 // 如果位置参数个数为 1,并且没有传递关键字参数,那么直接返回对象的类型 if (nargs == 1 && (kwds == NULL || !PyDict_GET_SIZE(kwds))) { // Py_TYPE 负责获取对象类型,因此相当于 type(args[0]) obj = (PyObject *) Py_TYPE(PyTuple_GET_ITEM(args, 0)); // 增加引用计数,返回 obj return Py_NewRef(obj); } // 如果位置参数的个数不等于 1,那么一定等于 3 if (nargs != 3) { PyErr_SetString(PyExc_TypeError, "type() takes 1 or 3 arguments"); return NULL; } } // 接下来执行类型对象(也可能是元类)的 tp_new,也就是 __new__ // 如果不存在,那么会报错,而在 Python 中见到的报错信息就是这里指定的 if (type->tp_new == NULL) { _PyErr_Format(tstate, PyExc_TypeError, "cannot create '%s' instances", type->tp_name); return NULL; } // 执行类型对象的 __new__ obj = type->tp_new(type, args, kwds); // 检测调用是否正常,如果调用正常,那么 obj 一定指向一个合法的 PyObject // 而如果 obj 为 NULL,则表示执行出错,此时解释器会抛出异常 obj = _Py_CheckFunctionResult(tstate, (PyObject*)type, obj, NULL); if (obj == NULL) return NULL; // __new__ 执行完之后该执行啥了,显然是 __init__,但需要先做一个检测 // 如果 __new__ 返回的实例对象的类型不是当前类型,那么直接返回,不再执行 __init__ // 比如自定义 class A,那么在 __new__ 里面应该返回 A 的实例对象,但假设返回个 123 // 由于返回值的类型不是当前类型,那么不再执行初始化函数 __init__ if (!PyObject_TypeCheck(obj, type)) return obj; // 走到这里说明类型一致,那么执行 __init__,将 obj、args、kwds 一起传过去 type = Py_TYPE(obj); if (type->tp_init != NULL) { int res = type->tp_init(obj, args, kwds); if (res < 0) { assert(_PyErr_Occurred(tstate)); Py_SETREF(obj, NULL); } else { assert(!_PyErr_Occurred(tstate)); } } // 返回创建的对象 obj return obj; }
所以整个过程就三步:
- 如果传递的是元类,并且只有一个参数,那么直接返回对象的类型;
- 否则先调用 tp_new 为实例对象申请内存;
- 再调用 tp_init(如果有)进行初始化,设置对象属性;
所以这对应了 Python 中的 __new__ 和 __init__,其中 __new__ 负责为实例对象开辟一份内存,然后返回指向对象的指针,并且该指针会自动传递给 __init__ 中的 self。
class Girl: def __new__(cls, name, age): print("__new__ 方法执行啦") # 调用 object.__new__(cls) 创建 Girl 的实例对象 # 然后该对象的指针会自动传递给 __init__ 中的 self return object.__new__(cls) def __init__(self, name, age): print("__init__ 方法执行啦") self.name = name self.age = age g = Girl("古明地觉", 16) print(g.name, g.age) """ __new__ 方法执行啦 __init__ 方法执行啦 古明地觉 16 """
__new__ 里面的参数要和 __init__ 里面的参数保持一致,因为会先执行 __new__,然后解释器再将 __new__ 的返回值和传递的参数组合起来一起传给 __init__。因此从这个角度讲,设置属性完全可以在 __new__ 里面完成。
class Girl: def __new__(cls, name, age): self = object.__new__(cls) self.name = name self.age = age return self g = Girl("古明地觉", 16) print(g.name, g.age) """ 古明地觉 16 """
这样也是没问题的,不过 __new__ 一般只负责创建实例,设置属性应该交给 __init__ 来做,毕竟一个是构造函数、一个是初始化函数,各司其职。另外由于 __new__ 里面不负责初始化,那么它的参数除了 cls 之外,一般都会写成 *args 和 **kwargs。
然后再回过头来看一下 type_call 中的这两行代码:
tp_new 应该返回该类型对象的实例对象,而且一般情况下我们是不重写 __new__ 的,会默认执行 object 的 __new__。但如果我们重写了,那么必须要手动返回 object.__new__(cls)。可如果我们不返回,或者返回其它的话,会怎么样呢?
class Girl: def __new__(cls, *args, **kwargs): print("__new__ 方法执行啦") instance = object.__new__(cls) # 打印看看 instance 到底是个啥 print("instance:", instance) print("type(instance):", type(instance)) # 正确做法是将 instance 返回 # 但是我们不返回,而是返回一个整数 123 return 123 def __init__(self, name, age): print("__init__ 方法执行啦") g = Girl() """ __new__ 方法执行啦 instance: <__main__.Girl object at 0x0000019A2B7270A0> type(instance): <class '__main__.Girl'> """
这里面有很多可以说的点,首先就是 __init__ 里面需要两个参数,但是我们没有传,却还不报错。原因就在于这个 __init__ 压根就没有执行,因为 __new__ 返回的不是 Girl 的实例对象。
通过打印 instance,我们知道了 object.__new__(cls) 返回的就是 cls 的实例对象,而这里的 cls 就是 Girl 这个类本身。所以我们必须要返回 instance,才会自动执行相应的 __init__。
我们在外部来打印一下创建的实例对象吧,看看结果:
class Girl: def __new__(cls, *args, **kwargs): return 123 def __init__(self, name, age): print("__init__ 方法执行啦") g = Girl() print(g) """ 123 """
我们看到打印的结果是 123,所以再次总结一下 tp_new 和 tp_init 之间的区别,当然也对应 __new__ 和 __init__ 的区别:
- tp_new:为实例对象申请内存,底层会调用 tp_alloc,至于对象的大小则记录在 tp_basicsize 字段中,而在 Python 里面则是调用 object.__new__(cls),然后返回;
- tp_init:tp_new 的返回值会自动传递给 self,然后为 self 绑定相应的属性,也就是进行实例对象的初始化;
但如果 tp_new 返回的对象的类型不对,比如 type_call 的第一个参数接收的是 &PyList_Type,但 tp_new 返回的却是 PyTupleObject *,那么此时就不会执行 tp_init。
对应上面的 Python 代码就是,Girl 的 __new__ 应该返回 Girl 的实例对象(指针)才对,但却返回了整数,因此类型不一致,不会执行 __init__。
所以都说类在实例化的时候会先调用 __new__,再调用 __init__,相信你应该知道原因了,因为在源码中先调用 tp_new,再调用 tp_init。所以源码层面表现出来的,和我们在 Python 层面看到的是一样的。
小结
到此,我们就从 Python 和解释器两个层面解释了对象是如何调用的,更准确的说我们是从解释器的角度对 Python 层面的知识进行了验证,通过 tp_new 和 tp_init 的关系,来了解 __new__ 和 __init__ 的关系。
当然对象调用还不止目前说的这么简单,更多的细节隐藏在了幕后。后续我们会循序渐进,一点点地揭开它的面纱,并且在这个过程中还会不断地学习到新的东西。比如说,实例对象在调用方法的时候会自动将实例本身作为参数传递给 self,那么它为什么会传递呢?解释器在背后又做了什么工作呢?这些在之后的文章中都会详细说明。