在上一篇文章中,我们分析了对象是如何创建的,主要有两种方式,一种是通过特定类型 API,另一种是通过调用类型对象。
对于内置类型的实例对象而言,这两种方式都是支持的,比如列表,我们既可以通过 [ ] 创建,也可以通过 list() 创建,前者是列表的特定类型 API,后者是调用类型对象。
但对于自定义类的实例对象而言,我们只能通过调用类型对象的方式来创建。一个对象如果可以被调用,那么这个对象就是 callable,否则就不是 callable。而决定一个对象是不是 callable,则取决于它的类型对象。
调用 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 具有二象性:
同理,由于 type 的类型还是 type,那么 type 既是 type 的类型对象,type 也是 type 的实例对象。虽然这里描述的有一些绕,但应该不难理解,而为了避免后续的描述出现歧义,这里我们做一个申明:
由于 type 的内部定义了 __call__ 函数,那么说明类型对象都是可调用的,因为调用类型对象就是调用元类 type 的 __call__ 函数。而实例对象能否调用就不一定了,这取决于它的类型对象是否定义了 __call__ 函数,因为调用一个对象,本质上是调用其类型对象内部的 __call__ 函数。
class A: passa = 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 函数,因为 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;}
所以整个过程就三步:
所以这对应了 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 = ageg = 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 selfg = 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 返回的对象的类型不对,比如 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,那么它为什么会传递呢?解释器在背后又做了什么工作呢?这些在之后的文章中都会详细说明。
本文链接://www.dmpip.com//www.dmpip.com/showinfo-26-90034-0.html当调用一个 Python 对象时,背后都经历了哪些过程?
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 编程语言 Kotlin 2.0.0 现已发布,K2 编译器进入稳定状态
下一篇: Rust 正在全面入侵前端