当我们在控制台敲下这个语句, Python 内部是如何从无到有创建一个浮点对象的?
|
|
另外,Python 又是怎么知道该如何将它打印到屏幕上的呢?
|
|
对象使用完毕, 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* 内部根据对象类型区别处理。
以对象打印函数为例:
|
|
接口第一个参数为待打印对象,可以是任意类型的对象,因此参数类型是 PyObject* 。 *Python* 内部一般都是通过 *PyObject** 引用对象,以达到泛型化的目的。
对于任意类型的对象,均可调用 PyObject_Print 将其打印出来:
|
|
PyObject_Print 接口内部根据对象类型,决定如何输出对象。
特型API
特型API 与类型相关,属于 具体对象层 ( Concrete Object Layer ),简称 COL 。 这类 API 只能作用于某种类型的对象,例如浮点对象 PyFloatObject 。 Python 内部为每一种内置对象提供了这样一组 API ,举例如下:
|
|
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 来创建:
|
|
例子中我们通过调用类型对象 float ,实例化了一个浮点实例 pi,对象居然还可以调用! 在 Python 中,可以被调用的对象就是 可调用对象 。
问题来了,可调用对象被调用时,执行什么函数呢? 由于类型对象保存着实例对象的元信息, float 类型对象的类型是 type ,因此秘密应该就隐藏在 type 中。
再次考察 PyType_Type ,我们找到了 tp_call 字段,这是一个函数指针:
|
|
当实例对象被调用时,便执行 tp_call 字段保存的处理函数。
因此,float(‘3.14’) 在 C 层面等价于:
|
|
即:
|
|
最终执行, type_call 函数:
|
|
调用参数通过 args 和 kwargs 两个对象传递,先不展开,留到函数机制中详细介绍。
那么,type_call 函数内部的运行原理是什么呢?点击 阅读原文,获取更多细节!
【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注: