Python 对象初探
在 Python 的世界一切皆对象,不论是整数,还是字符串,甚至连类型、函数等都是一种对象。
对象的分类
以下是 Python 对象的大致的一个分类
- Fundamental 对象: 类型对象
- Numeric 对象: 数值对象
- Sequence 对象: 容纳其他对象的序列集合对象
- Mapping 对象: 类似 C++中的 map 的关联对象
- Internal 对象: Python 虚拟机在运行时内部使用的对象
对象机制的基石 PyObject
对于初学者来说这么多类型的对象怎么学?别着急,我们后续章节会解答。
在开始我们的学习之旅之前,我们要先认识一个结构体PyObject,可以说 Python 的对象机制就是基于PyObject拓展开来的,所以我们先看看PyObject 到底长什么样。
源文件:
Include/object.h
1 | // Include/object.h |
经过宏替换后
1 | typedef struct _object { |
Python 中的所有对象都拥有一些相同的内容,而这些内容就定义在PyObject中,
PyObject 包含 一个用于垃圾回收的双向链表,一个引用计数变量 ob_refcnt
和 一个类型对象指针ob_type
引用计数表示该对象被变量引用的次数,对象被引用1次,ob_refcnt就会加1,引用解除时,ob_refcnt就会减少1。引用计数是Python实现对象回收的重要机制之一。
定长对象与变长对象
Python中根据对象所占用的内存空间大小是否固定,可以将对象分为定长对象和变长对象。
定长对象如整数。变长对象如字符串、列表等。字符串、列表中有多少个元素,都无法事先确定,只能使用变长对象来进行存储。
变长对象都拥有一个相同的内容 PyVarObject,而 PyVarObject也是基于PyObject扩展的。
从代码中可以看出PyVarObject比PyObject多出了一个用于存储元素个数的变量ob_size。
源文件:Include/object.h
1 | // Include/object.h |
宏替换后
1 |
类型对象
前面我们提到了PyObject 的 对象类型指针struct _typeobject *ob_type
,它指向的类型对象就决定了一个对象是什么类型的。
这是一个非常重要的结构体,它不仅仅决定了一个对象的类型,还包含大量的元信息
,
包括创建对象需要分配多少内存,对象都支持哪些操作等等。
接下来我们看一下struct _typeobject
代码
在 PyTypeObject 的定义中包含许多信息,主要分类以下几类:
- 类型名, tp_name, 主要用于 Python 内部调试用
- 创建该类型对象时分配的空间大小信息,即
tp_basicsize
和tp_itemsize
- 与该类型对象相关的操作信息(如
tp_print
这样的函数指针) - 一些对象属性
源文件:Include/object.h
1 | // Include/object.h |
类型的类型
在 PyTypeObjet 定义开始有一个宏PyOject_VAR_HEAD
,查看源码可知 PyTypeObjet 是一个变长对象
源文件:Include/object.h
1 | // Include/object.h |
对象的类型是由该对象指向的 类型对象 决定的,那么类型对象的类型是由谁决定的呢?
对于其他对象,可以通过与其关联的类型对象确定其类型,那么通过什么来确定一个对象是类型对象呢?
答案就是 PyType_Type
源文件:Objects/typeobject.c
1 | // Objects/typeobject.c |
PyType_Type
在类型机制中至关重要,所有用户自定义 class
所
对应的 PyTypeObject
对象都是通过 PyType_Type
创建的
接下来我们看 PyLong_Type
是怎么与 PyType_Type
建立联系的。
前面提到,在 Python 中,每一个对象都将自己的引用计数、类型信息保存在开始的部分中。
为了方便对这部分内存初始化,Python 中提供了几个有用的宏:
源文件:Include/object.h
1 | // Include/object.h |
这些宏在各种内建类型对象的初始化中被大量使用。
以PyLong_Type
为例,可以清晰的看到一般的类型对象和PyType_Type
之间的关系
源文件:Objects/longobject.c
1 | // Objects/longobject.c |
下图是对象运行时的图像表现
对象的创建
Python 创建对象有两种方式
范型 API 或称为 AOL (Abstract Object Layer)
这类 API 通常形如PyObject_XXX
这样的形式。可以应用在任何 Python 对象上,
如PyObject_New
。创建一个整数对象的方式
1 | PyObject* longobj = PyObject_New(Pyobject, &PyLong_Type); |
与类型相关的 API 或称为 COL (Concrete Object Layer)
这类 API 通常只能作用于某一种类型的对象上,对于每一种内建对象
Python 都提供了这样一组 API。例如整数对象,我们可以利用如下的 API 创建
1 | PyObject *longObj = PyLong_FromLong(10); |
对象的行为
在 PyTypeObject 中定义了大量的函数指针。这些函数指针可以视为类型对象中
所定义的操作,这些操作直接决定着一个对象在运行时所表现出的行为,比如 PyTypeObject 中的 tp_hash
指明了该类型对象如何生成其hash
值。
在PyTypeObject的代码中,我们还可以看到非常重要的三组操作族
PyNumberMethods *tp_as_number
PySequenceMethods *tp_as_sequence
PyMappingMethods *tp_as_mapping
PyNumberMethods 的代码如下
源文件:Include/object.h
1 | // Include/object.h |
PyNumberMethods 定义了一个数值对象该支持的操作。一个数值对象如 整数对象,那么它的类型对象 PyLong_Type
中tp_as_number.nb_add
就指定了它进行加法操作时的具体行为。
在以下代码中可以看出PyLong_Type
中的tp_as_number
项指向的是long_as_number
源文件:Objects/longobject.h
1 | // Objects/longobject.c |
PySequenceMethods *tp_as_sequence
和 PyMappingMethods *tp_as_mapping
的分析与PyNumberMethods *tp_as_number
相同,大家可以自行查阅源码
对象的多态性
Python 创建一个对象比如 PyLongObject 时,会分配内存进行初始化,然后
Python 内部会用 PyObject*
变量来维护这个对象,其他对象也与此类似
所以在 Python 内部各个函数之间传递的都是一种范型指针 PyObject*
我们不知道这个指针所指的对象是什么类型,只能通过所指对象的 ob_type
域
动态进行判断,而 Python 正是通过 ob_type
实现了多态机制
考虑以下的 calc_hash 函数
1 | Py_hash_t |
如果传递给 calc_hash 函数的指针是一个 PyLongObject*
,那么它会调用 PyLongObject 对象对应的类型对象中定义的 hash 操作tp_hash
,tp_hash
可以在PyTypeObject中找到,
而具体赋值绑定我们可以在 PyLong_Type
初始化代码中看到绑定的是long_hash
函数
源文件:Objects/longobject.c
1 | // Objects/longobject.c |
如果指针是一个 PyUnicodeObject*
,那么就会调用 PyUnicodeObject 对象对应的类型对象中定义的 hash 操作,查看源码可以看到 实际绑定的是 unicode_hash
函数
源文件:Objects/unicodeobject.c
1 | // Objects/unicodeobject.c |
引用计数
Python 通过引用计数来管理维护对象在内存中的存在与否
Python 中的每个东西都是一个对象, 都有ob_refcnt
变量,这个变量维护对象的引用计数,从而最终决定该对象的创建与销毁
在 Python 中,主要通过 Py_INCREF(op)
与Py_DECREF(op)
这两个宏
来增加和减少对一个对象的引用计数。当一个对象的引用计数减少到 0 之后,Py_DECREF
将调用该对象的tp_dealloc
来释放对象所占用的内存和系统资源;
但这并不意味着最终一定会调用 free
释放内存空间。因为频繁的申请、释放内存会大大降低 Python 的执行效率。因此 Python 中大量采用了内存对象池的技术,使得对象释放的空间归还给内存池而不是直接free
,后续使用可先从对象池中获取
源文件:Include/object.h
1 | // Include/object.h |