Python对象初探

cooolr 于 2022-08-05 发布

1.png

1.1 Python 内的对象

1.1.1 对象机制的基石——PyObject

在 Python 中,所有的东西都是对象,而所有的对象都拥有一些相同的内容,这些内 容在 PyObject 中定义, PyObject 是整个 Python 对象机制的核心。

[object.h]
#define **PyObject_HEAD**
    _PyObject_HEAD_EXTRA
    int ob_refcnt; //对象的引用计数
    struct _typeobject *ob_type; //对象类型的类型对象

typedef struct _object {
    PyObject_HEAD 
} **PyObject**;
[intobject.h]
typedef struct {
    PyObject_HEAD
    long ob_ival;
} **PyIntObject**;

Python 的整数对象中,除了PyObject ,还有一个额外的 long 变量,我们说一个整数当然应该有一个值,这个“值”的信息就保存在 ob_ival 中。同样,成千上万的其他对象,都在 PyObject 之外保存了属于自己的特殊的信息。

1.1.2 定长对象和变长对象

整数对象的特殊信息是一个 C 中的整形变量,无论这个整数对象的值有多大,都可以保存在这个整形变量 (ob_ival) 中。但是对于另一类像字符串对象、list 对象、dict 等对象来说,就没这么幸运了。

因此,Python 在 PyObject 对象之外,还有一个表示这类对象的结构体—— PyVarObject :

[object.h]
#define **PyObject_VAR_HEAD**
    PyObject_HEAD
    int ob_size; /* Number of items in variable part */

typedef struct {
    PyObject_VAR_HEAD
} **PyVarObject**;

我们把整数对象这样不包含可变长度数据的对象称为“定长对象”,而字符串对象这样包含可变长度数据的对象称为“变长对象”,它们的区别在于定长对象的不同对象占用的内存大小是一样的,而变长对象的不同对象占用的内存可能是不一样的。

比如,整数对象“1”和“100”占用的内存大小都是 sizeof(PyIntObject) ,而字符串对象“Python”和“Ruby”占用的内存大小就不同了。正是这种区别导致了 PyVarObject 对象中 ob_size的出现。变长对象通常都是容器, ob_size 这个成员实际上就是指明了变长对象中一共容纳了多少个元素。比如对于 Python 中最常用的 list ,它就是一个 PyVarObject 对象,如果某一时刻,这个 list中有 5 个元素,那么 ob_size 的值就是 5。

从 PyObject_VAR_HEAD 的定义可以看出, PyVarObject 实际上只是对 PyObject 的一个扩展而已。因此,对于任何一个 PyVarObject ,其所占用的内存,开始部分的字节的意义和PyObject 是一样的。换句话说,在 Python 内部,每一个对象都拥有相同的对象头部。这就使得在 Python 中,对对象的引用变得非常的统一,我们只需要用一个 PyObject*指针就可以引用任意的一个对象。而不论该对象实际是一个什么对象。

图 1-1 显示了 Python 中不同对象与 PyObject 、 PyVarObject 在内存布局上的关系:

Snipaste_2022-08-04_16-24-57.png

1.2 类型对象

实际上,占用内存空间的大小是对象的一种元信息,这样的元信息是与对象所属类型密切相关的,因此它一定会出现在与对象所对应的类型对象中。现在我们可以来详细考察一下类型对象_typeobject:

	[object.h]
	typedef struct _typeobject {
	PyObject_VAR_HEAD
	char *tp_name; /* 类型名 常用于调试 格式为"<module>.<name>" */
	int tp_basicsize, tp_itemsize; /* 创建该类型对象时分配内存空间大小的信息 */
	/* 与该类型对象相关联的操作信息 */
	destructor tp_dealloc;
	printfunc tp_print;
	……
	/* More standard operations (here for binary compatibility) */
	hashfunc tp_hash;
	ternaryfunc tp_call;
	……
	} **PyTypeObject**;

事实上,一个 PyTypeObject 对象就是 Python 中对面向对象理论中“类”这个概念的实现,包含有属性、方法和构造器。

1.2.1 对象的创建

Python 对外提供了 C API,让用户可以从 C 环境中与 Python 交互,实际上,因为 Python 本身也是 C 写成的,所以 Python 内部也大量使用了这些 API。

一类称为范型的 API,这类 API 都具有诸如PyObject_*** 的形式,可以应用在任何 Python 对象身上,比如输出对象的 PyObject_Print ,你可以 PyObject_Print(int object) ,也可以 PyObject_Print(string object) ,API 内部会有一整套机制确定最终调用的函数是哪一个。

对于创建一个整数对象,我们可以采用如下的表达式:

PyObject* intObj = PyObject_New(PyObject,&PyInt_Type) 。

另一类是与类型相关的 API,或者称为 COL(Concrete Object Layer)。这类 API 通常只能作用在某一种类型的对象上,对于每一种内建对象,Python 都提供了这样的一组 API。

不论采用哪种 C API,Python 内部最终都是直接分配内存,因为 Python 对于内建对象是无所不知的。但是对于用户自定义的类型,比如通过 class A(object) ,定义的一个类型 A ,如果要创建 A 的实例对象(前面我们已经提到,实例对象可以视为面向对象理论中的“对象”概念),Python 就不可能事先提供 PyA_New 这样的 API。对于这种情况,Python会通过 A 所对应的类型对象创建实例对象。

Snipaste_2022-08-04_18-21-24.png

标上序号的虚线箭头代表了创建整数对象的函数调用流程,首先 PyInt_Type 中的tp_new 会被调用,如果这个 tp_new 为 NULL (真正的 PyInt_Type 中并不为 NULL ,我们这里只是举例说明 tp_new 为 NULL 的情况),那么会到 tp_base 指定的基类中去寻找tp_new 操作, PyBaseObject_Type 的 tp_new 指向了 object_new 。在 Python 2.2 之后的 new style class 中,所有的类都是以 object 为基类的,所以最终会找到一个不为 NULL 的tp_new 。在 object_new 中,会访问 PyInt_Type 中记录的tp_basicsize 信息,继而完成申请内存的操作。这个信息记录着一个整数对象应该占用多大内存,在 Python 源码中, 你会看到这个值被设置成了 sizeof(PyIntObject) 。在调用 tp_new 完成“创建对象”之后,流程会转向 PyInt_Type 的 tp_init ,完成“初始化对象”的工作。对应到 C++中,tp_new 可以视为 new 操作符,而 tp_init 则可视为类的构造函数。

1.2.2 对象的行为

在 PyTypeObject 中定义了大量的函数指针,这些函数指针最终都会指向某个函数,或者指向 NULL 。这些函数指针可以视为类型对象中所定义的操作,而这些操作直接决定着一个对象在运行时所表现出的行为。

比如 PyTypeObject 中的 tp_hash 指明对于该类型的对象,如何生成其 hash 值。我们看到 tp_hash 是一个 hashfunc 类型的变量,在 object.h 中, hashfunc 实际上是一个函数指针: typedef long (*hashfunc)(PyObject *) 。在上一节中我们还看到了 tp_new ,tp_init 是如何决定一个实例对象被创建出来并初始化的。在 PyTypeObject 中指定的不同的操作信息也正是一种对象区别于另一种对象的关键所在。

在这些操作信息中,有三组非常重要的操作族,在 PyTypeObject 中,它们是 tp_as_number 、 tp_as_sequence 、 tp_as_mapping 。它们分别指向 PyNumberMethods 、 PySequenceMethods 和 PyMappingMethods 函数族,我们可以看一看 PyNumberMethods 这个函数族:

[object.h]
typedef PyObject * (*binaryfunc)(PyObject *, PyObject *);

typedef struct {
    binaryfunc nb_add;
    binaryfunc nb_subtract;
    ……
} **PyNumberMethods**;

在 PyNumberMethods 中,定义了作为一个数值对象应该支持的操作。如果一个对象 能被视为数值对象,比如整数,整数对象当然是一个数值对象。那么在其对应的类型对象 PyInt_Type 中, tp_as_number.nb_add 就指定了对该对象进行加法操作时的具体行为。 同样, PySequenceMethods 和PyMappingMethods 中分别定义了作为一个序列对象和关联对象,应该支持的行为,这两种对象的典型例子是 list 和 dict 。

对于一种类型来说,它完全可以同时定义三个函数族中的所有操作。换句话说,一个 对象可以既表现出数值对象的特性,也可以表现出关联对象的特性。图 1-4 给出了这样一 个例子:

Snipaste_2022-08-05_11-15-38.png

1.2.3 类型的类型

仔细观察 PyTypeObject, 会有一个有趣的发现。在 PyTypeObject 定义的最开始, 可以发现 PyObject_VAR_HEAD ,这意味着 Python 中的类型实际上也是一个对象。没错, 在 Python 中,任何一个东西都是对象,而每一个对象都是对应一种类型的,那么一个有 趣的问题就出现了,类型对象的类型是什么呢?这个问题听上去很绕口,实际上却非常重 要,对于其他的对象,可以通过与其关联的类型对象确定其类型,那么通过什么来确定一 个对象是类型对象呢?答案就是 PyType_Type :

[typeobject.c]
PyTypeObject **PyType_Type** = {
    PyObject_HEAD_INIT(&PyType_Type)
    0, /* ob_size */
    "type", /* tp_name */
    sizeof(PyHeapTypeObject), /* tp_basicsize */
    sizeof(PyMemberDef), /* tp_itemsize */
    ……
};

PyType_Type 在 Python 的类型机制中是一个至关重要的对象,所有用户自定义 class 所对应的 PyTypeObject 对象都是通过这个对象创建的。图 1-5 中显示了一般的 PyType- Object 和 PyType_Type 的关系:

Snipaste_2022-08-05_11-23-26.png

图 1-5 中那个一再出现的 <type ‘type’> 就是 Python 内部的 PyType_Type ,它是所 有 class 的 class,所以它在 Python 中被称为 metaclass 。

我们接着来看 PyInt_Type 是怎么和 PyType_Type 建立关系的。前面提到,在 Python 中,每一个对象都将自己的引用计数、类型信息保存在开始的部分中。为了方便对这部分 内存的初始化,Python 中提供了几个有用的宏:

[object.h]
#ifdef Py_TRACE_REFS
    #define _PyObject_EXTRA_INIT 0, 0,
#else
    #define _PyObject_EXTRA_INIT
#endif
#define **PyObject_HEAD_INIT**(type) \
_PyObject_EXTRA_INIT \
1, type,

再回顾一下 PyObject 和 PyVarObject 的定义,初始化的动作就一目了然了。实际上, 这些宏在各种内建类型对象的初始化中被大量地使用着。 以 PyInt_Type 为例,可以更清晰地看到一般的类型对象和这个特立独行的 PyType_ Type 对象之间的关系:

[intobject.c]
PyTypeObject **PyInt_Type** = {
    PyObject_HEAD_INIT(&PyType_Type)
    0,
    "int",
    sizeof(PyIntObject),
    ……
} ;

现在我们可以想象,看到一个整数对象在运行时的形象的表示了,如图 1-6 所示:

Snipaste_2022-08-05_11-26-33.png

1.3 Python 对象的多态性

通过 PyObject 和 PyTypeObject ,Python 利用 C 语言完成了 C++所提供的对象的多 态的特性。在 Python 创建一个对象,比如 PyIntObject 对象时,会分配内存,进行初始 化。然后 Python 内部会用一个 PyObject* 变量,而不是通过一个 PyIntObject* 变量来保 存和维护这个对象。其他对象也与此类似,所以在 Python 内部各个函数之间传递的都是 一种范型指针—— PyObject* 。这个指针所指的对象究竟是什么类型的,我们不知道,只 能从指针所指对象的 ob_type 域动态进行判断,而正是通过这个域,Python 实现了多态 机制。

考虑下面的 Print 函数:

void Print(PyObject* object)
{
object->ob_type->tp_print(object);
}

如果传给 Print 的指针是一个 PyIntObject* ,那么它就会调用 PyIntObject 对象对 应的类型对象中定义的输出操作,如果指针是一个 PyStringObject* ,那么就会调用 PyStringObject 对象对应的类型对象中定义的输出操作。可以看到,这里同一个函数在 不同情况下表现出了不同的行为,这正是多态的核心所在。 前面已经提到的 AOL 的 C API 正是建立在这中“多态”机制之上的。下面给出了一 个简单的例子:

[object.c]
long PyObject_Hash(PyObject *v)
{
PyTypeObject *tp = v->ob_type;
if (tp->tp_hash != NULL)
return (*tp->tp_hash)(v);
……
}

1.4 引用计数

Python 通过对一个对象的引用计数的管理来维护对象在内存中的存在与否。我们知道 在 Python 中每一个东西都是一个对象,都有一个 ob_refcnt 变量。这个变量维护着该对 象的引用计数,从而也最终决定着该对象的创建与消亡。 在 Python 中,主要是通过 Py_INCREF(op) 和 Py_DECREF(op) 两个宏来增加和减少一

个对象的引用计数。当一个对象的引用计数减少到 0 之后, Py_DECREF 将调用该对象的析 构函数来释放该对象所占有的内存和系统资源。注意这里的“析构函数”借用了 C++的词 汇,实际上这个析构动作是通过在对象对应的类型对象中定义的一个函数指针来指定的, 就是那个 tp_dealloc 。

如果熟悉设计模式中的 Observer 模式,就可以看到,这里隐隐约约透着 Observer 模 式的影子。在 ob_refcnt 减为 0 之后,将触发对象销毁的事件。从 Python 的对象体系来 看,各个对象提供了不同的事件处理函数,而事件的注册动作正是在各个对象对应的类型 对象中静态完成的。 PyObject 中的 ob_refcnt 是一个 32 位的整形变量,这实际蕴含着 Python 所做的一 个假设,即对一个对象的引用不会超过一个整形变量的最大值。一般情况下,如果不是恶 意代码,这个假设显然是成立的。 需要注意的是,在 Python 的各种对象中,类型对象是超越引用计数规则的。类型对 象“跳出三界外,不再五行中”,永远不会被析构。每一个对象中指向类型对象的指针不 被视为对类型对象的引用。 在每一个对象创建的时候,Python 提供了一个_ Py_NewReference(op) 宏来将对象的 引用计数初始化为 1。 在 Python 的源代码中可以看到,在不同的编译选项下 (Py_REF_DEBUG, Py_TRACE_ REFS) ,引用计数的宏还要做许多额外的工作。下面展示的代码是 Python 在最终发行时这 些宏所对应的实际的代码:

[object.h]
#define _Py_NewReference(op) ((op)->ob_refcnt = 1)
#define _Py_Dealloc(op) ((*(op)->ob_type->tp_dealloc)((PyObject *)(op)))
#define Py_INCREF(op) ((op)->ob_refcnt++)
#define Py_DECREF(op) \
if (--(op)->ob_refcnt != 0) \
; \
else \
_Py_Dealloc((PyObject *)(op))
/* Macros to use in case the object pointer may be NULL: */
#define Py_XINCREF(op) if ((op) == NULL) ; else Py_INCREF(op)
#define Py_XDECREF(op) if ((op) == NULL) ; else Py_DECREF(op)`

在一个对象的引用计数减为 0 时,与该对象对应的析构函数就会被调用,但是要特别 注意的是,调用析构函数并不意味着最终一定会调用 free 释放内存空间,如果真是这样 的话,那频繁地申请、释放内存空间会使 Python 的执行效率大打折扣(更何况 Python 已 经多年背负了人们对其执行效率的不满)。一般来说,Python 中大量采用了内存对象池的 技术,使用这种技术可以避免频繁地申请和释放内存空间。因此在析构时,通常都是将对象占用的空间归还到内存池中。这一点在接下来对 Python 内建对象的实现中可以看得一 清二楚。

1.5 Python 对象的分类

我们将 Python 的对象从概念上大致分为 5 类,需要指出的是,这种分类并不一定完 全正确,不过是提供一种看待 Python 中对象的视角而已。  Fundamental 对象:类型对象  Numeric 对象:数值对象  Sequence 对象:容纳其他对象的序列集合对象  Mapping 对象:类似于 C++中 map 的关联对象  Internal 对象:Python 虚拟机在运行时内部使用的对象

图 1-7 列出了我们的对象分类体系,并给出了每一个类别中的一些实例:

Snipaste_2022-08-05_11-30-44.png