当调用一个 Python 对象时,背后都经历了哪些过程?
楔子
在上一篇文章中,我们分析了对象是如何创建的,主要有两种方式,一种是通过特定类型 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 都做了哪些事情呢?
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。
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__ 里面完成。
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)。可如果我们不返回,或者返回其它的话,会怎么样呢?
这里面有很多可以说的点,首先就是 __init__ 里面需要两个参数,但是我们没有传,却还不报错。原因就在于这个 __init__ 压根就没有执行,因为 __new__ 返回的不是 Girl 的实例对象。
通过打印 instance,我们知道了 object.__new__(cls) 返回的就是 cls 的实例对象,而这里的 cls 就是 Girl 这个类本身。所以我们必须要返回 instance,才会自动执行相应的 __init__。
我们在外部来打印一下创建的实例对象吧,看看结果:
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,那么它为什么会传递呢?解释器在背后又做了什么工作呢?这些在之后的文章中都会详细说明。
本站大部分文章、数据、图片均来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了您的权益请来信告知我们删除。邮箱:1451803763@qq.com