从创建到销毁,对象的生命周期

当我们在控制台敲下这个语句, Python 内部是如何从无到有创建一个浮点对象的?

>>> pi = 3.14

另外,Python 又是怎么知道该如何将它打印到屏幕上的呢?

>>> print(pi)
3.14

对象使用完毕, Python 必须将其销毁,销毁的时机又该如何确定呢? 带着这些问题,接着考察对象在从创建到销毁整个生命周期中的行为表现,从中探寻答案。

以下讨论以一个足够简单的类型 float 为例,对应的 C 实体是 PyFloat_Type 。

C API

开始讨论对象创建前,先介绍 Python 提供的 C API 。

Python 是用 C 写成的,对外提供了 C API ,让用户可以从 C 环境中与其交互。 Python 内部也大量使用这些 API ,为了更好研读源码,先系统了解 API 组成结构很有必要。 C API 分为两类: 泛型API 以及 特型API 。

泛型API

泛型API 与类型无关,属于 抽象对象层 ( Abstract Object Layer ),简称 AOL 。 这类 API 参数是 PyObject* ,可处理任意类型的对象, API 内部根据对象类型区别处理。

以对象打印函数为例:

int
PyObject_Print(PyObject *op, FILE *fp, int flags)

接口第一个参数为待打印对象,可以是任意类型的对象,因此参数类型是 PyObject* 。 Python 内部一般都是通过 PyObject* 引用对象,以达到泛型化的目的。

对于任意类型的对象,均可调用 PyObject_Print 将其打印出来:

// 打印浮点对象
PyObject *fo = PyFloat_FromDouble(3.14);
PyObject_Print(fo, stdout, 0);

// 打印整数对象
PyObject *lo = PyLong_FromLong(100);
PyObject_Print(lo, stdout, 0);

PyObject_Print 接口内部根据对象类型,决定如何输出对象。

特型API

特型API 与类型相关,属于 具体对象层 ( Concrete Object Layer ),简称 COL 。 这类 API 只能作用于某种类型的对象,例如浮点对象 PyFloatObject 。 Python 内部为每一种内置对象提供了这样一组 API ,举例如下:

PyObject *
PyFloat_FromDouble(double fval)

PyFloat_FromDouble 创建一个浮点对象,并将它初始化为给定值 fval

对象的创建

经过前面的理论学习,我们知道对象的 元数据 保存在对应的 类型对象 中,元数据当然也包括 对象如何创建 的信息。 因此,有理由相信 实例对象 由 类型对象 创建。

不管创建对象的流程如何,最终的关键步骤都是 分配内存 。 Python 对 内建对象 是无所不知的,因此可以提供 C API,直接分配内存并执行初始化。 以 PyFloat_FromDouble 为例,在接口内部为 PyFloatObject 结构体分配内存,并初始化相关字段即可。

对于用户自定义的类型 class Dog(object) , Python 就无法事先提供 PyDog_New 这样的 C API 了。 这种情况下,就只能通过 Dog 所对应的类型对象创建实例对象了。 至于需要分配多少内存,如何进行初始化,答案就需要在 类型对象 中找了。

总结起来,Python 内部一般通过这两种方法创建对象:

  • 通过 C API ,例如 PyFloat_FromDouble ,多用于内建类型;

  • 通过类型对象,例如 Dog ,多用于自定义类型;

通过类型对象创建实例对象,是一个更通用的流程,同时支持内置类型和自定义类型。 以创建浮点对象为例,我们还可以通过浮点类型 PyFloat_Type 来创建:

>>> pi = float('3.14')
>>> pi
3.14

例子中我们通过调用类型对象 float ,实例化了一个浮点实例 pi,对象居然还可以调用! 在 Python 中,可以被调用的对象就是 可调用对象 。

问题来了,可调用对象被调用时,执行什么函数呢? 由于类型对象保存着实例对象的元信息, float 类型对象的类型是 type ,因此秘密应该就隐藏在 type 中。

再次考察 PyType_Type ,我们找到了 tp_call 字段,这是一个函数指针:

PyTypeObject PyType_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "type",                                     /* tp_name */
    sizeof(PyHeapTypeObject),                   /* tp_basicsize */
    sizeof(PyMemberDef),                        /* tp_itemsize */

    // ...
    (ternaryfunc)type_call,                     /* tp_call */

    // ...
};

当实例对象被调用时,便执行 tp_call 字段保存的处理函数。

因此,float(‘3.14’) 在 C 层面等价于:

PyFloat_Type.ob_type.tp_call(&PyFloat_Type, args, kwargs)

即:

PyType_Type.tp_call(&PyFloat_Type, args, kwargs)

最终执行, type_call 函数:

type_call(&PyFloat_Type, args, kwargs)

调用参数通过 args 和 kwargs 两个对象传递,先不展开,留到函数机制中详细介绍。

那么,type_call 函数内部的运行原理是什么呢? 阅读 更多章节,获取更多细节!

更多章节

洞悉 Python 虚拟机运行机制,探索高效程序设计之道!

到底如何才能提升我的 Python 开发水平,向更高一级的岗位迈进? 如果你有这些问题或者疑惑,请订阅我们的专栏,阅读更多章节:

https://cdn.fasionchan.com/python-source-course-qrcode.png

附录

订阅更新,获取更多学习资料,请关注我们的 微信公众号

微信搜索:小菜学编程

创作不易,如果觉得我们写得还行,就请我们喝杯咖啡吧😋

微信搜索:小菜学编程